mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97255ec90d | ||
|
|
cfccc87b82 | ||
|
|
09b4f8367e | ||
|
|
45481f9758 | ||
|
|
1119008317 | ||
|
|
123a0c1b7e | ||
|
|
2f21fa62b1 | ||
|
|
8ad449c6b7 | ||
|
|
fe8ec50806 | ||
|
|
86cefeb259 | ||
|
|
034e0a1e7f | ||
|
|
a6b776915f | ||
|
|
4ae52d0e3d | ||
|
|
f62720ef5a | ||
|
|
af09aaedb3 | ||
|
|
184616e874 | ||
|
|
6bb09c4e99 | ||
|
|
81add86c61 | ||
|
|
4bb1c784d8 | ||
|
|
322082c28b | ||
|
|
5db1d21aa5 | ||
|
|
0db5585b20 | ||
|
|
10ec54345a | ||
|
|
27b4710264 | ||
|
|
4132e92da5 | ||
|
|
264340a717 | ||
|
|
1d09c2c97d | ||
|
|
72cff1df1a | ||
|
|
6912e453ef | ||
|
|
839cbac47b | ||
|
|
5ba16dbc67 | ||
|
|
896b456dd3 | ||
|
|
f2c0202f06 | ||
|
|
497bac1cbf | ||
|
|
5b9734b60b | ||
|
|
3ac70d8977 | ||
|
|
32adc788d0 | ||
|
|
e465ed57cb | ||
|
|
df819fc232 | ||
|
|
539e201972 | ||
|
|
c14b3e179b | ||
|
|
f093197dea | ||
|
|
13834039a8 | ||
|
|
4d3023f7f1 | ||
|
|
eb7d3b5ecd | ||
|
|
8c721759bd | ||
|
|
c6a741bb8c | ||
|
|
af5b76fe9f | ||
|
|
aa428b7571 | ||
|
|
ba68f40922 | ||
|
|
1380b023ba | ||
|
|
adfc79abd4 | ||
|
|
92e6ecd03c | ||
|
|
05546f0bfd | ||
|
|
6c5c99b5d1 | ||
|
|
f27ea08839 | ||
|
|
15f47aea81 | ||
|
|
fd23f031b6 | ||
|
|
a74c434718 | ||
|
|
29950a7629 | ||
|
|
0fd33a95f5 | ||
|
|
843ed0d0aa | ||
|
|
bf2cde45c6 | ||
|
|
ecab803cfa | ||
|
|
570ff126a0 |
No files matched your search
@@ -0,0 +1,64 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
staging_only:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
description: "Only move the latest-eas-build-staging tag, not the latest-eas-build tag"
|
||||
version:
|
||||
type: string
|
||||
required: true
|
||||
description: "Version number to move the tag to"
|
||||
dry_run:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Run the workflow in dry run mode, without making any NPM changes"
|
||||
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
|
||||
jobs:
|
||||
move-tag:
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
scope: "expo"
|
||||
node-version: 22
|
||||
- name: Resolve version (fails if the version does not exist)
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm pack --dry-run eas-cli@$INPUT_VERSION
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build-staging"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$INPUT_STAGING_ONLY" == "true" ]]; then
|
||||
echo "Only moving the staging tag, skipping the production tag"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['18', '20', '22']
|
||||
node: ['20', '22']
|
||||
include:
|
||||
- node: '20'
|
||||
coverage: true
|
||||
|
||||
+120
-2
@@ -12,11 +12,129 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove preview label from EAS Hosting commands ([#3241](https://github.com/expo/eas-cli/pull/3241) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [16.25.0](https://github.com/expo/eas-cli/releases/tag/v16.25.0) - 2025-10-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas:new` command changes (project name prompt, output cleanup, additional project files, additional prompts). ([#3235](https://github.com/expo/eas-cli/pull/3235) by [@mackenco](https://github.com/mackenco))
|
||||
- `eas:build` command displays a warning when account is near it's build limit. ([#3236](https://github.com/expo/eas-cli/pull/3236) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:run can use --wait and --json together. ([#3239](https://github.com/expo/eas-cli/pull/3239) by [@douglowder](https://github.com/douglowder))
|
||||
- Add missing env for local builds with latest plugin. ([#3237](https://github.com/expo/eas-cli/pull/3237) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Document the default values for `track` and `releaseStatus`. ([#3229](https://github.com/expo/eas-cli/pull/3229) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.24.1](https://github.com/expo/eas-cli/releases/tag/v16.24.1) - 2025-10-19
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert change in ora version. ([#3231](https://github.com/expo/eas-cli/pull/3231) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.24.0](https://github.com/expo/eas-cli/releases/tag/v16.24.0) - 2025-10-19
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- workflow:run --wait shows periodic job and step status updates. ([#3227](https://github.com/expo/eas-cli/pull/3227) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:logs should show logs from build and repack jobs. ([#3226](https://github.com/expo/eas-cli/pull/3226) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.23.1](https://github.com/expo/eas-cli/releases/tag/v16.23.1) - 2025-10-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.23.0](https://github.com/expo/eas-cli/releases/tag/v16.23.0) - 2025-10-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
|
||||
|
||||
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support `_expo/.routes.json` metadata file exported by `@expo/cli` for headers configuration on static deployments ([#3211](https://github.com/expo/eas-cli/pull/3211) by [@kitten](https://github.com/kitten))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add thorough tests for the env commands ([#3217](https://github.com/expo/eas-cli/pull/3217) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.4](https://github.com/expo/eas-cli/releases/tag/v16.20.4) - 2025-10-08
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Allow workflow:run to run a workflow from a GitHub ref. ([#3203](https://github.com/expo/eas-cli/pull/3203) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix --environment flag in eas exec and pull ([#3216](https://github.com/expo/eas-cli/pull/3216) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.3](https://github.com/expo/eas-cli/releases/tag/v16.20.3) - 2025-10-07
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix --environment flag in eas env commands ([#3213](https://github.com/expo/eas-cli/pull/3213) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.2](https://github.com/expo/eas-cli/releases/tag/v16.20.2) - 2025-10-07
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Create an `EnvironmentVariableEnvironment` enum and use it instead of the graphQL Enum ([#3208](https://github.com/expo/eas-cli/pull/3208) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.1](https://github.com/expo/eas-cli/releases/tag/v16.20.1) - 2025-10-03
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- skip syncing capabilities with settings if they don't contain the `enabled` attribute ([#3201](https://github.com/expo/eas-cli/pull/3201) by [@vonovak](https://github.com/vonovak))
|
||||
|
||||
## [16.20.0](https://github.com/expo/eas-cli/releases/tag/v16.20.0) - 2025-09-30
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:validate. ([#3165](https://github.com/expo/eas-cli/pull/3165) by [@douglowder](https://github.com/douglowder))
|
||||
- Add `eas:new` command behind `hidden` flag. ([#3196](https://github.com/expo/eas-cli/pull/3196) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.19.3](https://github.com/expo/eas-cli/releases/tag/v16.19.3) - 2025-09-17
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Temporarily remove warnings when no compatible build found, need to refine the message. ([#3182](https://github.com/expo/eas-cli/pull/3182) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [16.19.2](https://github.com/expo/eas-cli/releases/tag/v16.19.2) - 2025-09-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add artifact URLs to workflow:view. ([#3175](https://github.com/expo/eas-cli/pull/3175) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.19.1](https://github.com/expo/eas-cli/releases/tag/v16.19.1) - 2025-09-12
|
||||
|
||||
## [16.19.0](https://github.com/expo/eas-cli/releases/tag/v16.19.0) - 2025-09-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Allow any string as Android release `track`, not just the `production`, `internal`, `alpha`, `beta`. ([#3163](https://github.com/expo/eas-cli/pull/3163) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [16.18.1](https://github.com/expo/eas-cli/releases/tag/v16.18.1) - 2025-09-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
|
||||
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
|
||||
- Better description for update:rollback. ([#3154](https://github.com/expo/eas-cli/pull/3154) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.18.0](https://github.com/expo/eas-cli/releases/tag/v16.18.0) - 2025-08-27
|
||||
@@ -27,7 +145,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make EXPO_PUBLIC_ env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Make `EXPO_PUBLIC_` env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.17.4](https://github.com/expo/eas-cli/releases/tag/v16.17.4) - 2025-07-31
|
||||
|
||||
|
||||
+14
-5
@@ -1,12 +1,21 @@
|
||||
# Development
|
||||
|
||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
## Getting Started
|
||||
|
||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
If this is your first time running `eas-cli` locally, make sure you install and build the packages. In the project root, run:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
From there, set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
> The only exception is if there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
|
||||
Open your `.zshrc` or other config file and add:
|
||||
|
||||
```
|
||||
```sh
|
||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
||||
```
|
||||
|
||||
@@ -14,7 +23,7 @@ Then use it with `easd` like `easd build`.
|
||||
|
||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
||||
|
||||
```
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -33,7 +42,7 @@ If it is easier for you to work with the code locally with compiler options set
|
||||
|
||||
you can consider using:
|
||||
|
||||
```
|
||||
```sh
|
||||
yarn start-allow-unused
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "16.18.1",
|
||||
"version": "16.25.1",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+221
-212
File diff suppressed because it is too large.
Load diff
Generated
+932
-154
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "16.18.1",
|
||||
"version": "16.25.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -12,11 +12,11 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.173",
|
||||
"@expo/eas-json": "16.18.0",
|
||||
"@expo/eas-build-job": "1.0.243",
|
||||
"@expo/eas-json": "16.25.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "1.0.117",
|
||||
"@expo/logger": "1.0.221",
|
||||
"@expo/multipart-body-parser": "2.0.0",
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.7.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "1.0.173",
|
||||
"@expo/steps": "1.0.231",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
@@ -85,13 +85,14 @@
|
||||
"tar": "6.2.1",
|
||||
"tar-stream": "3.1.7",
|
||||
"terminal-link": "2.1.1",
|
||||
"ts-deepmerge": "6.2.0",
|
||||
"tslib": "2.6.2",
|
||||
"turndown": "7.1.2",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.1",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.6.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
@@ -126,7 +127,6 @@
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.4.0",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "6.2.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.3.3"
|
||||
@@ -226,7 +226,7 @@
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsc --project tsconfig.build.json && yarn copy-new-templates",
|
||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
|
||||
@@ -235,7 +235,8 @@
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"verify-graphql-code": "./scripts/verify-graphql.sh",
|
||||
"clean": "rimraf dist build tmp node_modules yarn-error.log"
|
||||
"clean": "rimraf dist build tmp node_modules yarn-error.log",
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
|
||||
@@ -192,9 +192,16 @@ export function getMockWorkflowRunsFragment(
|
||||
failures?: number;
|
||||
pending?: number;
|
||||
withJobs?: number;
|
||||
withBuildJobs?: number;
|
||||
}
|
||||
): WorkflowRunFragment[] {
|
||||
const { successes = 0, failures = 0, pending = 0, withJobs = 0 } = params ?? {};
|
||||
const {
|
||||
successes = 0,
|
||||
failures = 0,
|
||||
pending = 0,
|
||||
withJobs = 0,
|
||||
withBuildJobs = 0,
|
||||
} = params ?? {};
|
||||
const runs: any[] = [];
|
||||
for (let i = 0; i < successes; i++) {
|
||||
runs.push({
|
||||
@@ -247,10 +254,13 @@ export function getMockWorkflowRunsFragment(
|
||||
for (let i = 0; i < withJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithJobsFragment());
|
||||
}
|
||||
for (let i = 0; i < withBuildJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithBuildJobsFragment());
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
export function getMockWorkflowJobFragment(
|
||||
export function getMockWorkflowCustomJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
@@ -258,7 +268,7 @@ export function getMockWorkflowJobFragment(
|
||||
key: 'job1',
|
||||
name: 'job1',
|
||||
status: WorkflowJobStatus.Success,
|
||||
type: WorkflowJobType.Build,
|
||||
type: WorkflowJobType.Custom,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
outputs: {},
|
||||
@@ -270,6 +280,19 @@ export function getMockWorkflowJobFragment(
|
||||
id: 'job1',
|
||||
logFileUrls: ['https://example.com/log1'],
|
||||
errors: [],
|
||||
artifacts: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowBuildJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
...getMockWorkflowCustomJobFragment(runId),
|
||||
type: WorkflowJobType.Build,
|
||||
outputs: {
|
||||
build_id: 'build1',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -302,6 +325,38 @@ export function getMockWorkflowRunWithJobsFragment(
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowJobFragment(id)],
|
||||
jobs: [getMockWorkflowCustomJobFragment(id)],
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunWithBuildJobsFragment(
|
||||
runID?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
|
||||
const id = runID ?? 'build1';
|
||||
return {
|
||||
id,
|
||||
status: WorkflowRunStatus.Failure,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
errors: [],
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
app: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowBuildJobFragment(id)],
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import {
|
||||
getMockEmptyWorkflowRunsFragment,
|
||||
getMockWorkflowJobFragment,
|
||||
getMockWorkflowCustomJobFragment,
|
||||
getMockWorkflowRunWithJobsFragment,
|
||||
getMockWorkflowRunsFragment,
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
} from './utils';
|
||||
import { fetchRawLogsForJobAsync } from '../../commandUtils/workflow/fetchLogs';
|
||||
import {
|
||||
fetchRawLogsForBuildJobAsync,
|
||||
fetchRawLogsForCustomJobAsync,
|
||||
} from '../../commandUtils/workflow/fetchLogs';
|
||||
import WorkflowLogView from '../../commands/workflow/logs';
|
||||
import { AppPlatform, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log from '../../log';
|
||||
@@ -20,6 +25,7 @@ jest.mock('../../build/ios/version');
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../graphql/queries/AppQuery');
|
||||
jest.mock('../../graphql/queries/BuildQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowJobQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowRunQuery');
|
||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
||||
@@ -35,7 +41,7 @@ jest.mock('../../commandUtils/workflow/fetchLogs');
|
||||
|
||||
describe(WorkflowLogView, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowJobFragment());
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowCustomJobFragment());
|
||||
jest
|
||||
.mocked(WorkflowRunQuery.withJobsByIdAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
|
||||
@@ -98,7 +104,7 @@ describe(WorkflowLogView, () => {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest.mocked(fetchRawLogsForJobAsync).mockResolvedValue(null);
|
||||
jest.mocked(fetchRawLogsForCustomJobAsync).mockResolvedValue(null);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
@@ -130,7 +136,64 @@ describe(WorkflowLogView, () => {
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForJobAsync)
|
||||
.mocked(fetchRawLogsForCustomJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(promptAsync).toHaveBeenCalledTimes(3);
|
||||
expect(Log.error).not.toHaveBeenCalled();
|
||||
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
|
||||
});
|
||||
test('view build logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ withBuildJobs: 1 }));
|
||||
jest.mocked(BuildQuery.byIdAsync).mockResolvedValue({
|
||||
id: 'build1',
|
||||
status: BuildStatus.Finished,
|
||||
priority: BuildPriority.Normal,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
isForIosSimulator: false,
|
||||
project: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
slug: 'app',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
platform: AppPlatform.Android,
|
||||
logFiles: ['https://example.com/log1'],
|
||||
});
|
||||
let promptCalls = 0;
|
||||
jest.mocked(promptAsync).mockImplementation(async () => {
|
||||
if (promptCalls === 0) {
|
||||
promptCalls++;
|
||||
return { selectedRun: 'build1' };
|
||||
} else if (promptCalls === 1) {
|
||||
promptCalls++;
|
||||
return { selectedJob: 0 };
|
||||
} else {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForBuildJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
|
||||
@@ -108,3 +108,7 @@ export function getEASUpdateURL(projectId: string, manifestHostOverride: string
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
}
|
||||
}
|
||||
|
||||
export function getExpoApiWorkflowSchemaURL(): string {
|
||||
return getExpoApiBaseUrl() + '/v2/workflows/schema';
|
||||
}
|
||||
@@ -72,6 +72,7 @@ function createMockBuildFragment({
|
||||
updatedAt: new Date().toISOString(),
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
logFiles: [],
|
||||
priority: BuildPriority.Normal,
|
||||
project: {
|
||||
__typename: 'App',
|
||||
|
||||
@@ -70,6 +70,7 @@ function createMockBuildFragment({
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
logFiles: [],
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
priority: BuildPriority.Normal,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Android, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import getenv from 'getenv';
|
||||
@@ -11,7 +11,7 @@ import { resolveRemoteVersionCodeAsync } from './version';
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import {
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
@@ -124,10 +123,9 @@ export async function prepareAndroidBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
@@ -30,7 +29,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
BuildJob,
|
||||
FingerprintSource,
|
||||
Metadata,
|
||||
Platform,
|
||||
} from '@expo/eas-build-job';
|
||||
@@ -50,9 +49,11 @@ import { createFingerprintAsync } from '../fingerprint/cli';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildMetadataInput,
|
||||
BuildParamsInput,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
FingerprintSourceInput,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
@@ -94,7 +95,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob
|
||||
sendBuildRequestAsync(
|
||||
appId: string,
|
||||
job: TJob,
|
||||
metadata: Metadata,
|
||||
metadata: BuildMetadataInput,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult>;
|
||||
}
|
||||
@@ -193,13 +194,20 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
|
||||
buildId: nullthrows(process.env.EAS_BUILD_ID),
|
||||
metadata: transformMetadata(metadata),
|
||||
metadata: transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
|
||||
}),
|
||||
});
|
||||
printJsonOnlyOutput({ job, metadata });
|
||||
return undefined;
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
const graphqlMetadata = transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
|
||||
});
|
||||
return await sendBuildRequestAsync(builder, job, graphqlMetadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
@@ -362,7 +370,7 @@ async function sendBuildRequestAsync<
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
@@ -376,7 +384,7 @@ async function sendBuildRequestAsync<
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata,
|
||||
graphqlMetadata,
|
||||
buildParams
|
||||
);
|
||||
|
||||
@@ -654,7 +662,7 @@ async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends
|
||||
): Promise<{
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
fingerprintSource?: FingerprintSource | undefined;
|
||||
fingerprintSource?: FingerprintSourceInput | undefined;
|
||||
}> {
|
||||
const runtimeAndFingerprintMetadata =
|
||||
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
|
||||
@@ -676,7 +684,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{
|
||||
runtimeVersion?: string;
|
||||
fingerprintSource?: FingerprintSource;
|
||||
fingerprintSource?: FingerprintSourceInput;
|
||||
fingerprintHash?: string;
|
||||
}> {
|
||||
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
|
||||
@@ -723,7 +731,7 @@ async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends
|
||||
{ debug }: { debug?: boolean } = {}
|
||||
): Promise<{
|
||||
fingerprintHash?: string;
|
||||
fingerprintSource?: FingerprintSource;
|
||||
fingerprintSource?: FingerprintSourceInput;
|
||||
}> {
|
||||
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
|
||||
workflow: ctx.workflow,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { isEnvironment } from './utils/environment';
|
||||
import { DefaultEnvironment } from './utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
|
||||
@@ -49,20 +48,9 @@ async function resolveEnvVarsAsync({
|
||||
projectId: string;
|
||||
}): Promise<Env> {
|
||||
const environment =
|
||||
buildProfile.environment?.toUpperCase() ??
|
||||
buildProfile.environment ??
|
||||
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
Log.log(
|
||||
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
|
||||
buildProfile.env && Object.keys(buildProfile.env).length > 0
|
||||
? Object.keys(buildProfile.env).join(', ')
|
||||
: 'no environment variables specified'
|
||||
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
|
||||
);
|
||||
return { ...buildProfile.env };
|
||||
}
|
||||
|
||||
try {
|
||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
|
||||
graphqlClient,
|
||||
@@ -129,16 +117,16 @@ async function resolveEnvVarsAsync({
|
||||
|
||||
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
|
||||
buildProfile: BuildProfile
|
||||
): EnvironmentVariableEnvironment {
|
||||
): DefaultEnvironment {
|
||||
const environment =
|
||||
buildProfile.distribution === 'store'
|
||||
? EnvironmentVariableEnvironment.Production
|
||||
? DefaultEnvironment.Production
|
||||
: buildProfile.developmentClient
|
||||
? EnvironmentVariableEnvironment.Development
|
||||
: EnvironmentVariableEnvironment.Preview;
|
||||
? DefaultEnvironment.Development
|
||||
: DefaultEnvironment.Preview;
|
||||
|
||||
Log.log(
|
||||
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
|
||||
`Resolved "${environment}" environment for the build. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
|
||||
)}`
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
ArchiveSourceType,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
FingerprintSourceType,
|
||||
Metadata,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
FingerprintSourceInput,
|
||||
BuildMode as GraphQLBuildMode,
|
||||
BuildTrigger as GraphQLBuildTrigger,
|
||||
FingerprintSourceType as GraphQLFingeprintSourceType,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
WorkerLoggerLevel,
|
||||
@@ -42,11 +40,16 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
}
|
||||
}
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
export function transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource,
|
||||
}: {
|
||||
metadata: Metadata;
|
||||
fingerprintSource: FingerprintSourceInput | undefined;
|
||||
}): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
fingerprintSource:
|
||||
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
|
||||
fingerprintSource,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
@@ -57,20 +60,6 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
};
|
||||
}
|
||||
|
||||
export function transformFingerprintSource(
|
||||
fingerprintSource: NonNullable<Metadata['fingerprintSource']>
|
||||
): FingerprintSourceInput | null {
|
||||
if (fingerprintSource.type !== FingerprintSourceType.GCS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: GraphQLFingeprintSourceType.Gcs,
|
||||
bucketKey: fingerprintSource.bucketKey,
|
||||
isDebugFingerprint: fingerprintSource.isDebugFingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
function transformCredentialsSource(
|
||||
credentialsSource: Metadata['credentialsSource']
|
||||
): BuildCredentialsSource {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
@@ -7,7 +7,7 @@ import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
import { resolveRemoteBuildNumberAsync } from './version';
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
|
||||
@@ -15,7 +15,6 @@ import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
@@ -106,10 +105,9 @@ export async function prepareIosBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
@@ -32,7 +31,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { Job, Metadata, version } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
import { getExpoApiBaseUrl } from '../api';
|
||||
import Log from '../log';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '1.0.171';
|
||||
const PLUGIN_PACKAGE_VERSION = version; // should match version of @expo/eas-build-job
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
@@ -60,6 +61,7 @@ export async function runLocalBuildAsync(
|
||||
...env,
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
__API_SERVER_URL: getExpoApiBaseUrl(),
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FingerprintSource, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -22,7 +22,6 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
runtimeAndFingerprintMetadata: {
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
fingerprintSource?: FingerprintSource | undefined;
|
||||
}
|
||||
): Promise<Metadata> {
|
||||
const channelObject = await resolveChannelAsync(ctx);
|
||||
@@ -36,7 +35,6 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
|
||||
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
||||
...channelObject,
|
||||
distribution,
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
|
||||
type Environment = NonNullable<BuildProfile['environment']>;
|
||||
|
||||
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
|
||||
production: EnvironmentVariableEnvironment.Production,
|
||||
preview: EnvironmentVariableEnvironment.Preview,
|
||||
development: EnvironmentVariableEnvironment.Development,
|
||||
};
|
||||
|
||||
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
|
||||
return Object.values(EnvironmentVariableEnvironment).includes(
|
||||
env as EnvironmentVariableEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
export function buildProfileEnvironmentToEnvironment(
|
||||
environment: BuildProfile['environment']
|
||||
): EnvironmentVariableEnvironment | null {
|
||||
if (!environment) {
|
||||
return null;
|
||||
}
|
||||
return BuildProfileEnvironmentToEnvironment[environment];
|
||||
export enum DefaultEnvironment {
|
||||
Development = 'development',
|
||||
Preview = 'preview',
|
||||
Production = 'production',
|
||||
}
|
||||
@@ -8,7 +8,42 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { link } from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
|
||||
|
||||
export function formatGraphQLBuildArtifacts(build: BuildFragment): FormatFieldsItem[] {
|
||||
const fields = [
|
||||
{
|
||||
label: 'Application Archive URL',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
default: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? link(url) : 'null';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Build Artifacts URL',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
default: {
|
||||
const url = build.artifacts?.buildArtifactsUrl;
|
||||
return url ? link(url) : 'null';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
return fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
const actor = getActorName(build);
|
||||
@@ -84,27 +119,7 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
label: 'Logs',
|
||||
value: link(getBuildLogsUrl(build)),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
case GraphQLBuildStatus.PendingCancel:
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? link(url) : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
...formatGraphQLBuildArtifacts(build),
|
||||
{
|
||||
label: 'Fingerprint',
|
||||
get value() {
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
import Log, { link } from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import { Client } from '../vcs/vcs';
|
||||
@@ -60,7 +59,7 @@ interface BaseGetContextAsyncArgs {
|
||||
|
||||
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
|
||||
extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
|
||||
withServerSideEnvironment: string | null;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
|
||||
@@ -11,7 +10,7 @@ export interface ContextOptions {
|
||||
/**
|
||||
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
|
||||
*/
|
||||
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
|
||||
withServerSideEnvironment?: string | null;
|
||||
}
|
||||
|
||||
export default abstract class ContextField<T> {
|
||||
|
||||
+4
-12
@@ -1,23 +1,15 @@
|
||||
import { ExpoGraphqlClient } from './createGraphqlClient';
|
||||
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
|
||||
const cachedServerSideEnvironmentVariables: Record<
|
||||
EnvironmentVariableEnvironment,
|
||||
Record<string, string> | null
|
||||
> = {
|
||||
[EnvironmentVariableEnvironment.Development]: null,
|
||||
[EnvironmentVariableEnvironment.Preview]: null,
|
||||
[EnvironmentVariableEnvironment.Production]: null,
|
||||
};
|
||||
const cachedServerSideEnvironmentVariables: Record<string, Record<string, string> | undefined> = {};
|
||||
|
||||
export async function loadServerSideEnvironmentVariablesAsync({
|
||||
environment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
}: {
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
projectId: string;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}): Promise<Record<string, string>> {
|
||||
@@ -42,13 +34,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
||||
|
||||
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
|
||||
// are used in tandem, it saves on unnecessary typying in commands
|
||||
async function upperCaseAsync<T>(input: string): Promise<T> {
|
||||
return input.toUpperCase() as T;
|
||||
}
|
||||
|
||||
function mapToLowercase<T extends string>(options: T[]): T[] {
|
||||
return options.map(option => option.toLowerCase()) as T[];
|
||||
}
|
||||
|
||||
export const EasNonInteractiveAndJsonFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
@@ -23,21 +11,17 @@ export const EasNonInteractiveAndJsonFlags = {
|
||||
};
|
||||
|
||||
export const EasEnvironmentFlagParameters = {
|
||||
description: "Environment variable's environment",
|
||||
parse: upperCaseAsync,
|
||||
options: mapToLowercase([
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
EnvironmentVariableEnvironment.Preview,
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
]),
|
||||
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
|
||||
};
|
||||
|
||||
export const EASEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
|
||||
environment: Flags.string({
|
||||
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
|
||||
}),
|
||||
};
|
||||
|
||||
export const EASMultiEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
multiple: true,
|
||||
}),
|
||||
@@ -81,12 +65,10 @@ export const EasJsonOnlyFlag = {
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution.',
|
||||
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
|
||||
parse: upperCaseAsync,
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
|
||||
required: false,
|
||||
default: null,
|
||||
default: undefined,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
|
||||
import { installDependenciesAsync } from '../../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../../onboarding/runCommand';
|
||||
import {
|
||||
cloneTemplateAsync,
|
||||
initializeGitRepositoryAsync,
|
||||
installProjectDependenciesAsync,
|
||||
} from '../commands';
|
||||
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../onboarding/installDependencies');
|
||||
jest.mock('../../../ora');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('commands', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('cloneTemplateAsync', () => {
|
||||
it('should clone the template project with ssh', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'ssh',
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
|
||||
it('should clone the template project with https', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(false);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'https',
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('installProjectDependenciesAsync', () => {
|
||||
it('should install the project dependencies', async () => {
|
||||
const projectDir = '/test/project-directory';
|
||||
|
||||
jest.mocked(installDependenciesAsync).mockResolvedValue();
|
||||
jest.mocked(runCommandAsync).mockResolvedValue();
|
||||
|
||||
await installProjectDependenciesAsync(projectDir, 'npm');
|
||||
|
||||
expect(installDependenciesAsync).toHaveBeenCalledWith({
|
||||
outputLevel: 'none',
|
||||
projectDir,
|
||||
packageManager: 'npm',
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', 'expo-updates'],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', '@expo/metro-runtime'],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeGitRepositoryAsync', () => {
|
||||
it('should initialize git repository', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await initializeGitRepositoryAsync(projectDir);
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['init'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['add', '.'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { LogSpy } from './testUtils';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
findAvailableProjectNameAsync,
|
||||
generateProjectConfigAsync,
|
||||
getAccountChoices,
|
||||
promptForProjectAccountAsync,
|
||||
} from '../configs';
|
||||
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../utils', () => ({
|
||||
printDirectory: jest.fn((dir: string) => dir),
|
||||
}));
|
||||
|
||||
describe('configs', () => {
|
||||
let logSpy: LogSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
logSpy = new LogSpy('withInfo');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.restore();
|
||||
});
|
||||
|
||||
// Helper function to verify prompt was called with expected message
|
||||
const expectPromptToHaveMessage = (expectedMessage: string): void => {
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expectedMessage,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const mockUserInput = (inputValue: any): void => {
|
||||
jest.mocked(promptAsync).mockResolvedValue(inputValue);
|
||||
};
|
||||
|
||||
describe('generateProjectConfigAsync', () => {
|
||||
const mockOptions = {
|
||||
graphqlClient: {} as ExpoGraphqlClient,
|
||||
projectAccount: 'test-account',
|
||||
};
|
||||
|
||||
describe('when no path is provided', () => {
|
||||
it('should prompt for project name with default value', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'expo-project' });
|
||||
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'expo-project',
|
||||
projectDirectory: expect.stringContaining('/expo-project'),
|
||||
});
|
||||
expectPromptToHaveMessage('What would you like to name your project?');
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initial: 'expo-project',
|
||||
})
|
||||
);
|
||||
logSpy.expectLogToContain('Using project name: expo-project');
|
||||
});
|
||||
|
||||
it('should prompt and use custom name entered by user', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'user-custom-name' });
|
||||
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'user-custom-name',
|
||||
projectDirectory: expect.stringContaining('/user-custom-name'),
|
||||
});
|
||||
expectPromptToHaveMessage('What would you like to name your project?');
|
||||
logSpy.expectLogToContain('Using project name: user-custom-name');
|
||||
});
|
||||
|
||||
it('should use shortid when prompted name exists', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true); // base name exists
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'existing-project' });
|
||||
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^existing-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining(result.projectName),
|
||||
});
|
||||
logSpy.expectLogToContain('Using project name:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when path is provided', () => {
|
||||
it('should use absolute path and extract name from basename', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
const absolutePath = '/absolute/path/to/my-project';
|
||||
const result = await generateProjectConfigAsync(absolutePath, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-project',
|
||||
projectDirectory: absolutePath,
|
||||
});
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
|
||||
it('should resolve relative path and extract name from basename', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
const relativePath = 'some/relative/my-app';
|
||||
const result = await generateProjectConfigAsync(relativePath, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-app',
|
||||
projectDirectory: expect.stringContaining('some/relative/my-app'),
|
||||
});
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptForProjectAccountAsync', () => {
|
||||
it('should prompt for a project account when there are multiple accounts', async () => {
|
||||
const selectedAccount = 'jester';
|
||||
mockUserInput({ account: { name: selectedAccount } });
|
||||
|
||||
const result = await promptForProjectAccountAsync(jester);
|
||||
|
||||
expect(result).toBe(selectedAccount);
|
||||
expectPromptToHaveMessage('Which account should own this project?');
|
||||
});
|
||||
|
||||
it('should automatically select single account with permissions', async () => {
|
||||
const result = await promptForProjectAccountAsync(jester);
|
||||
expect(result).toBe(jester.accounts[0].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccountChoices', () => {
|
||||
it('should return mapped account choices', () => {
|
||||
const result = getAccountChoices(jester);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
title: 'jester (personal account)',
|
||||
value: { name: 'jester' },
|
||||
},
|
||||
{
|
||||
title: 'other',
|
||||
value: { name: 'other' },
|
||||
disabled: true,
|
||||
description:
|
||||
'You do not have the required permissions to create projects on this account.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAvailableProjectNameAsync', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockOptions = {
|
||||
graphqlClient: mockGraphqlClient,
|
||||
projectAccount: 'jester',
|
||||
};
|
||||
|
||||
it('should find first available name when checking both local and remote', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'test-project',
|
||||
projectDirectory: '/base/path/test-project',
|
||||
});
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(1);
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester',
|
||||
'test-project'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use shortid variant when base name exists locally', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true); // Base name exists locally
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(1);
|
||||
expect(fs.pathExists).toHaveBeenCalledWith('/base/path/test-project');
|
||||
});
|
||||
|
||||
it('should use shortid variant when base name exists remotely', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id'); // Base name exists remotely
|
||||
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(1);
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester',
|
||||
'test-project'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use shortid variant when base name is taken both locally and remotely', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id');
|
||||
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { getEASUpdateURL } from '../../../api';
|
||||
import { AppFragment } from '../../../graphql/generated';
|
||||
import { easCliVersion } from '../../../utils/easCli';
|
||||
import {
|
||||
cleanAndPrefix,
|
||||
copyProjectTemplatesAsync,
|
||||
generateAppConfigAsync,
|
||||
generateEasConfigAsync,
|
||||
updatePackageJsonAsync,
|
||||
updateReadmeAsync,
|
||||
} from '../projectFiles';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('projectFiles', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readJson).mockImplementation(() => Promise.resolve({}));
|
||||
jest.mocked(fs.writeJson).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
|
||||
jest.mocked(fs.symlink).mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
describe('cleanAndPrefix', () => {
|
||||
it('should clean and prefix the string', () => {
|
||||
expect(cleanAndPrefix('test-app-123', 'app')).toBe('testapp123');
|
||||
expect(cleanAndPrefix('test@app#with$symbols', 'app')).toBe('testappwithsymbols');
|
||||
expect(cleanAndPrefix('test-app-with-dashes', 'app')).toBe('testappwithdashes');
|
||||
expect(cleanAndPrefix('TestApp', 'app')).toBe('appTestApp');
|
||||
expect(cleanAndPrefix('100 App', 'app')).toBe('app100App');
|
||||
});
|
||||
|
||||
it('should handle schemes', () => {
|
||||
expect(cleanAndPrefix('test-app-123', 'scheme')).toBe('test-app-123');
|
||||
expect(cleanAndPrefix('test@app#with$symbols', 'scheme')).toBe('testappwithsymbols');
|
||||
expect(cleanAndPrefix('test+app.with-allowed.symbols', 'scheme')).toBe(
|
||||
'test+app.with-allowed.symbols'
|
||||
);
|
||||
expect(cleanAndPrefix('100 App', 'scheme')).toBe('scheme100app');
|
||||
expect(cleanAndPrefix('TestApp', 'scheme')).toBe('testapp');
|
||||
expect(cleanAndPrefix('testApp2', 'scheme')).toBe('testapp2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAppConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
const mockUpdateUrl = 'https://u.expo.dev/test-project-id';
|
||||
|
||||
const createMockApp = (overrides: Partial<AppFragment> = {}): AppFragment => ({
|
||||
id: 'test-project-id',
|
||||
name: 'Test App',
|
||||
fullName: '@testowner/test-app',
|
||||
slug: 'test-app',
|
||||
ownerAccount: {
|
||||
id: 'testowner-account-id',
|
||||
name: 'testowner',
|
||||
users: [],
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(getEASUpdateURL).mockReturnValue(mockUpdateUrl);
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {} as ExpoConfig,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate the app config', async () => {
|
||||
const mockApp = createMockApp();
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {
|
||||
name: 'value-to-override',
|
||||
slug: 'value-to-override',
|
||||
icon: 'value-to-keep',
|
||||
} as ExpoConfig,
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(getEASUpdateURL).toHaveBeenCalledWith('test-project-id', null);
|
||||
|
||||
const expectedConfig = {
|
||||
expo: {
|
||||
name: 'Test App',
|
||||
slug: 'test-app',
|
||||
scheme: 'testapp',
|
||||
icon: 'value-to-keep',
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: 'test-project-id',
|
||||
},
|
||||
},
|
||||
owner: 'testowner',
|
||||
updates: {
|
||||
url: mockUpdateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier: 'com.testowner.testapp',
|
||||
},
|
||||
android: {
|
||||
package: 'com.testowner.testapp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/app.json`, expectedConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the bundle identifier', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test-app-with-dashes',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-account-id',
|
||||
name: 'test-owner',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithdashes',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the owner account name', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-123-account-id',
|
||||
name: 'test-owner-123',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner123.testapp',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the slug', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test@app#with$symbols',
|
||||
fullName: '@testowner/test@app#with$symbols',
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithsymbols',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateEasConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
it('should generate the eas config', async () => {
|
||||
const expectedEasConfig = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: 'remote',
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: { simulator: true },
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
await generateEasConfigAsync(projectDir);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePackageJsonAsync', () => {
|
||||
it('should update the package.json', async () => {
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const projectDir = '/test/project-dir';
|
||||
await updatePackageJsonAsync(projectDir);
|
||||
|
||||
const expectedPackageJson = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
draft: 'npx eas-cli@latest workflow:run create-draft.yml',
|
||||
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
|
||||
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
|
||||
},
|
||||
};
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/package.json`, expectedPackageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyProjectTemplatesAsync', () => {
|
||||
it('should copy the project templates', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
expect(fs.copy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('templates'),
|
||||
projectDir,
|
||||
expect.objectContaining({
|
||||
errorOnExist: false,
|
||||
overwrite: true,
|
||||
filter: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateReadmeAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
const readmeTemplatePath = expect.stringContaining('templates/readme-additions.md');
|
||||
const projectReadmePath = `${projectDir}/README.md`;
|
||||
|
||||
const mockReadmeTemplate = `## Get started
|
||||
|
||||
To start the app, in your terminal run:
|
||||
|
||||
\`\`\`bash
|
||||
npm run start
|
||||
\`\`\`
|
||||
|
||||
## Workflows
|
||||
`;
|
||||
const mockExistingReadme = `# My App
|
||||
|
||||
## Get started
|
||||
|
||||
Follow these steps to get started with the app.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the dependencies first.`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(fs.readFile).mockImplementation((filePath: any) => {
|
||||
const content = filePath.includes('readme-additions.md')
|
||||
? mockReadmeTemplate
|
||||
: filePath === projectReadmePath
|
||||
? mockExistingReadme
|
||||
: '';
|
||||
return Promise.resolve(content);
|
||||
});
|
||||
});
|
||||
|
||||
it('should read the existing readme and merge the template content', async () => {
|
||||
await updateReadmeAsync(projectDir, 'npm');
|
||||
|
||||
expect(fs.readFile).toHaveBeenCalledWith(readmeTemplatePath, 'utf8');
|
||||
expect(fs.readFile).toHaveBeenCalledWith(projectReadmePath, 'utf8');
|
||||
|
||||
const headings = ['# My App', '## Get started', '## Workflows'];
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
projectReadmePath,
|
||||
expect.stringMatching(new RegExp(headings.join('[\\s\\S]*')))
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace npm run with package manager specific commands', async () => {
|
||||
await updateReadmeAsync(projectDir, 'yarn');
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
projectReadmePath,
|
||||
expect.stringContaining('yarn run start')
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle the case when 'Get started' section does not exist", async () => {
|
||||
const readmeWithoutGetStarted = `# My App
|
||||
|
||||
This is my awesome app.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the dependencies first.`;
|
||||
|
||||
jest.mocked(fs.readFile).mockImplementation((filePath: any) => {
|
||||
const content = filePath.includes('readme-additions.md')
|
||||
? mockReadmeTemplate
|
||||
: filePath === projectReadmePath
|
||||
? readmeWithoutGetStarted
|
||||
: '';
|
||||
return Promise.resolve(content);
|
||||
});
|
||||
|
||||
await updateReadmeAsync(projectDir, 'npm');
|
||||
|
||||
const headings = ['# My App', '## Installation', '## Get started', '## Workflows'];
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
projectReadmePath,
|
||||
expect.stringMatching(new RegExp(headings.join('[\\s\\S]*')))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import Log from '../../../log';
|
||||
|
||||
export class LogSpy {
|
||||
private readonly logSpy: jest.SpyInstance;
|
||||
|
||||
constructor(logMethod: 'warn' | 'log' | 'withTick' | 'withInfo') {
|
||||
this.logSpy = jest.spyOn(Log, logMethod);
|
||||
}
|
||||
|
||||
getLogOutput(): string[] {
|
||||
return this.logSpy.mock.calls.map(call => (call.length === 0 ? '' : call.join(' ')));
|
||||
}
|
||||
|
||||
expectLogToContain(message: string): void {
|
||||
const output = this.getLogOutput();
|
||||
// strip out ANSI codes and special characters like the tick
|
||||
const outputWithoutAnsi = output.map(line =>
|
||||
line.replace(/\x1b\[[0-9;]*m/g, '').replace(/✔\s*/, '')
|
||||
);
|
||||
expect(outputWithoutAnsi.some(line => line.includes(message))).toBeTruthy();
|
||||
}
|
||||
|
||||
restore(): void {
|
||||
this.logSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import path from 'path';
|
||||
|
||||
import { printDirectory } from '../utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('printDirectory', () => {
|
||||
let cwdSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock process.cwd() to return a predictable value
|
||||
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/Users/test/projects');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return "." for the current directory', () => {
|
||||
const result = printDirectory('/Users/test/projects');
|
||||
expect(result).toBe('.');
|
||||
});
|
||||
|
||||
it('should return relative path with "./" prefix for subdirectory', () => {
|
||||
const result = printDirectory('/Users/test/projects/my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should return relative path with "./" prefix for nested subdirectory', () => {
|
||||
const result = printDirectory('/Users/test/projects/nested/my-app');
|
||||
expect(result).toBe('./nested/my-app');
|
||||
});
|
||||
|
||||
it('should return absolute path for directory outside cwd', () => {
|
||||
const result = printDirectory('/Users/other/projects/my-app');
|
||||
expect(result).toBe('/Users/other/projects/my-app');
|
||||
});
|
||||
|
||||
it('should return absolute path for parent directory', () => {
|
||||
const result = printDirectory('/Users/test');
|
||||
expect(result).toBe('/Users/test');
|
||||
});
|
||||
|
||||
it('should handle relative paths and convert them correctly', () => {
|
||||
// Relative path resolves to absolute first, then converts back to relative
|
||||
const result = printDirectory('my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle relative paths with "./" already', () => {
|
||||
const result = printDirectory('./my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle relative paths with "../" (goes outside cwd)', () => {
|
||||
const result = printDirectory('../other-project');
|
||||
// This goes outside cwd, so should return absolute path
|
||||
expect(result).toBe('/Users/test/other-project');
|
||||
});
|
||||
|
||||
it('should handle complex relative paths', () => {
|
||||
const result = printDirectory('./nested/deep/project');
|
||||
expect(result).toBe('./nested/deep/project');
|
||||
});
|
||||
|
||||
it('should normalize paths correctly', () => {
|
||||
const result = printDirectory('/Users/test/projects/./my-app/../my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle paths in real cwd', () => {
|
||||
// Restore mock to test with real cwd
|
||||
cwdSpy.mockRestore();
|
||||
const realCwd = process.cwd();
|
||||
|
||||
// Test with current directory
|
||||
const result1 = printDirectory(realCwd);
|
||||
expect(result1).toBe('.');
|
||||
|
||||
// Test with subdirectory
|
||||
const subDir = path.join(realCwd, 'test-dir');
|
||||
const result2 = printDirectory(subDir);
|
||||
expect(result2).toBe('./test-dir');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { printDirectory } from './utils';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
|
||||
import { PackageManager, installDependenciesAsync } from '../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../onboarding/runCommand';
|
||||
import { ora } from '../../ora';
|
||||
|
||||
export async function cloneTemplateAsync(targetProjectDir: string): Promise<string> {
|
||||
const githubUsername = 'expo';
|
||||
const githubRepositoryName = 'expo-template-default';
|
||||
|
||||
const spinner = ora(
|
||||
`${chalk.bold(`Cloning the project to ${printDirectory(targetProjectDir)}`)}`
|
||||
).start();
|
||||
|
||||
const cloneMethod = (await canAccessRepositoryUsingSshAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
}))
|
||||
? 'ssh'
|
||||
: 'https';
|
||||
|
||||
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
targetProjectDir,
|
||||
cloneMethod,
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
spinner.succeed(`Cloned the project to ${printDirectory(finalTargetProjectDirectory)}`);
|
||||
|
||||
return finalTargetProjectDirectory;
|
||||
}
|
||||
|
||||
export async function installProjectDependenciesAsync(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const spinner = ora(`${chalk.bold('Installing project dependencies')}`).start();
|
||||
await installDependenciesAsync({
|
||||
outputLevel: 'none',
|
||||
projectDir,
|
||||
packageManager,
|
||||
});
|
||||
|
||||
const dependencies = ['expo-updates', '@expo/metro-runtime'];
|
||||
for (const dependency of dependencies) {
|
||||
spinner.text = `Installing ${chalk.bold(dependency)}`;
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', dependency],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
}
|
||||
spinner.succeed(`Installed project dependencies`);
|
||||
}
|
||||
|
||||
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
|
||||
const spinner = ora(`${chalk.bold('Initializing Git repository')}`).start();
|
||||
await fs.remove(path.join(projectDir, '.git'));
|
||||
|
||||
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
|
||||
|
||||
for (const args of commands) {
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'git',
|
||||
args,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
}
|
||||
spinner.succeed(`Initialized Git repository`);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
|
||||
import { printDirectory } from './utils';
|
||||
import { Role } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { Choice, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
function validateProjectPath(resolvedPath: string): void {
|
||||
const normalizedPath = path.normalize(resolvedPath);
|
||||
|
||||
// Check for path traversal attempts
|
||||
if (normalizedPath.includes('..')) {
|
||||
throw new Error(`Invalid project path: "${resolvedPath}". Path traversal is not allowed.`);
|
||||
}
|
||||
|
||||
// Ensure we're not trying to create a project in system directories
|
||||
const systemDirs = ['/bin', '/sbin', '/etc', '/usr', '/var', '/sys', '/proc', '/dev'];
|
||||
const isSystemDir = systemDirs.some(
|
||||
dir => normalizedPath === dir || normalizedPath.startsWith(dir + path.sep)
|
||||
);
|
||||
|
||||
if (isSystemDir) {
|
||||
throw new Error(
|
||||
`Invalid project path: "${resolvedPath}". Cannot create projects in system directories.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateProjectConfigAsync(
|
||||
pathArg: string | undefined,
|
||||
options: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectAccount: string;
|
||||
}
|
||||
): Promise<{
|
||||
projectName: string;
|
||||
projectDirectory: string;
|
||||
}> {
|
||||
let baseName = 'expo-project';
|
||||
let parentDirectory = process.cwd();
|
||||
|
||||
if (pathArg) {
|
||||
const resolvedPath = path.isAbsolute(pathArg) ? pathArg : path.resolve(process.cwd(), pathArg);
|
||||
validateProjectPath(resolvedPath);
|
||||
baseName = path.basename(resolvedPath);
|
||||
parentDirectory = path.dirname(resolvedPath);
|
||||
} else {
|
||||
baseName = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'What would you like to name your project?',
|
||||
initial: 'expo-project',
|
||||
})
|
||||
).name;
|
||||
}
|
||||
|
||||
// Find an available name checking both local filesystem and remote server
|
||||
const { projectName, projectDirectory } = await findAvailableProjectNameAsync(
|
||||
baseName,
|
||||
parentDirectory,
|
||||
options
|
||||
);
|
||||
|
||||
Log.withInfo(`Using project name: ${projectName}`);
|
||||
Log.withInfo(`Using project directory: ${printDirectory(projectDirectory)}`);
|
||||
|
||||
return {
|
||||
projectName,
|
||||
projectDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
function getAccountPermissionsMap(actor: Actor): Map<string, boolean> {
|
||||
const permissionsMap = new Map<string, boolean>();
|
||||
|
||||
for (const account of actor.accounts) {
|
||||
const hasPermission =
|
||||
account.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly;
|
||||
permissionsMap.set(account.name, hasPermission);
|
||||
}
|
||||
|
||||
return permissionsMap;
|
||||
}
|
||||
|
||||
export async function promptForProjectAccountAsync(actor: Actor): Promise<string> {
|
||||
const permissionsMap = getAccountPermissionsMap(actor);
|
||||
|
||||
// If only one account, use it (if has permissions)
|
||||
if (actor.accounts.length === 1) {
|
||||
const account = actor.accounts[0];
|
||||
|
||||
if (permissionsMap.get(account.name)) {
|
||||
return account.name;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`You don't have permission to create projects on your only available account (${account.name}).`
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple accounts - prompt user to select one with permissions
|
||||
return (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices: getAccountChoices(actor, permissionsMap),
|
||||
})
|
||||
).account.name;
|
||||
}
|
||||
|
||||
export function getAccountChoices(actor: Actor, permissionsMap?: Map<string, boolean>): Choice[] {
|
||||
const permissions = permissionsMap ?? getAccountPermissionsMap(actor);
|
||||
const sortedAccounts = [...actor.accounts].sort((a, _b) =>
|
||||
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
|
||||
);
|
||||
|
||||
return sortedAccounts.map(account => {
|
||||
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
||||
const accountDisplayName = isPersonalAccount
|
||||
? `${account.name} (personal account)`
|
||||
: account.name;
|
||||
const disabled = !permissions.get(account.name);
|
||||
|
||||
return {
|
||||
title: accountDisplayName,
|
||||
value: { name: account.name },
|
||||
...(disabled && {
|
||||
disabled: true,
|
||||
description: 'You do not have the required permissions to create projects on this account.',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyProjectDoesNotExistAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
{ silent = false }: { silent?: boolean } = {}
|
||||
): Promise<boolean> {
|
||||
const existingProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
accountName,
|
||||
projectName
|
||||
);
|
||||
|
||||
const doesNotExist = existingProjectId === null;
|
||||
if (!doesNotExist && !silent) {
|
||||
Log.warn(`Project @${accountName}/${projectName} already exists on the server.`);
|
||||
}
|
||||
|
||||
return doesNotExist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an available project name that doesn't conflict with either:
|
||||
* Local filesystem (directory already exists)
|
||||
* Remote server (project already exists on Expo)
|
||||
*/
|
||||
export async function findAvailableProjectNameAsync(
|
||||
baseName: string,
|
||||
parentDirectory: string,
|
||||
{
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectAccount: string;
|
||||
}
|
||||
): Promise<{ projectName: string; projectDirectory: string }> {
|
||||
let projectName = baseName;
|
||||
let projectDirectory = path.join(parentDirectory, projectName);
|
||||
|
||||
const localExists = await fs.pathExists(projectDirectory);
|
||||
|
||||
const remoteAvailable = await verifyProjectDoesNotExistAsync(
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
projectName
|
||||
);
|
||||
|
||||
if (localExists || !remoteAvailable) {
|
||||
projectName = `${baseName}-${nanoid(6)}`;
|
||||
projectDirectory = path.join(parentDirectory, projectName);
|
||||
}
|
||||
|
||||
return {
|
||||
projectName,
|
||||
projectDirectory,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import merge from 'ts-deepmerge';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { AppFragment } from '../../graphql/generated';
|
||||
import { PackageManager } from '../../onboarding/installDependencies';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
|
||||
// Android package names must start with a lowercase letter
|
||||
// schemes must start with a lowercase letter and can only contain lowercase letters, digits, "+", "." or "-"
|
||||
export function cleanAndPrefix(_string: string, type: 'user' | 'app' | 'scheme'): string {
|
||||
let string = _string;
|
||||
let pattern = /[^A-Za-z0-9]/g;
|
||||
if (type === 'scheme') {
|
||||
string = _string.toLowerCase();
|
||||
pattern = /[^a-z0-9+.-]/g;
|
||||
}
|
||||
|
||||
const prefix = /^[^a-z]/.test(string) ? type : '';
|
||||
const cleaned = string.replaceAll(pattern, '');
|
||||
|
||||
return prefix + cleaned;
|
||||
}
|
||||
|
||||
export async function generateAppConfigAsync(projectDir: string, app: AppFragment): Promise<void> {
|
||||
const user = cleanAndPrefix(app.ownerAccount.name, 'user');
|
||||
const slug = cleanAndPrefix(app.slug, 'app');
|
||||
const scheme = cleanAndPrefix(app.name ?? app.slug, 'scheme');
|
||||
|
||||
const bundleIdentifier = `com.${user}.${slug}`;
|
||||
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
|
||||
|
||||
const { expo: baseExpoConfig } = await fs.readJson(path.join(projectDir, 'app.json'));
|
||||
const expoConfig: ExpoConfig = {
|
||||
name: app.name ?? app.slug,
|
||||
slug: app.slug,
|
||||
scheme,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: app.id,
|
||||
},
|
||||
},
|
||||
owner: app.ownerAccount.name,
|
||||
updates: {
|
||||
url: updateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier,
|
||||
},
|
||||
android: {
|
||||
package: bundleIdentifier,
|
||||
},
|
||||
};
|
||||
const mergedConfig = merge(baseExpoConfig, expoConfig);
|
||||
|
||||
const appJsonPath = path.join(projectDir, 'app.json');
|
||||
await fs.writeJson(appJsonPath, { expo: mergedConfig }, { spaces: 2 });
|
||||
}
|
||||
|
||||
export async function generateEasConfigAsync(projectDir: string): Promise<void> {
|
||||
const easBuildGitHubConfig = {
|
||||
android: {
|
||||
image: 'latest',
|
||||
},
|
||||
ios: {
|
||||
image: 'latest',
|
||||
},
|
||||
};
|
||||
|
||||
const easJson: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: {
|
||||
simulator: true,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
const easJsonPath = path.join(projectDir, 'eas.json');
|
||||
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
export async function updatePackageJsonAsync(projectDir: string): Promise<void> {
|
||||
const packageJsonPath = path.join(projectDir, 'package.json');
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
packageJson.scripts.draft = 'npx eas-cli@latest workflow:run create-draft.yml';
|
||||
packageJson.scripts['development-builds'] =
|
||||
'npx eas-cli@latest workflow:run create-development-builds.yml';
|
||||
packageJson.scripts.deploy = 'npx eas-cli@latest workflow:run deploy-to-production.yml';
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
export async function copyProjectTemplatesAsync(projectDir: string): Promise<void> {
|
||||
const templatesSourceDir = path.join(__dirname, 'templates');
|
||||
|
||||
// Copy everything from templates to projectDir, skipping readme-additions.md
|
||||
await fs.copy(templatesSourceDir, projectDir, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
filter: (src: string) => {
|
||||
return !src.endsWith('readme-additions.md');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateReadmeAsync(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const readmeTemplatePath = path.join(__dirname, 'templates', 'readme-additions.md');
|
||||
const projectReadmePath = path.join(projectDir, 'README.md');
|
||||
|
||||
const readmeAdditions = await fs.readFile(readmeTemplatePath, 'utf8');
|
||||
const existingReadme = await fs.readFile(projectReadmePath, 'utf8');
|
||||
|
||||
const targetSection = '## Get started';
|
||||
const sectionIndex = existingReadme.indexOf(targetSection);
|
||||
|
||||
let mergedReadme: string;
|
||||
if (sectionIndex !== -1) {
|
||||
// Find the next ## section after "## Get started"
|
||||
const afterTargetSection = existingReadme.substring(sectionIndex);
|
||||
const nextSectionMatch = afterTargetSection.match(/\n## /);
|
||||
let endIndex = existingReadme.length;
|
||||
if (nextSectionMatch?.index !== undefined) {
|
||||
// Replace from "## Get started" to the next "##" section
|
||||
endIndex = sectionIndex + nextSectionMatch.index;
|
||||
}
|
||||
|
||||
const beforeSection = existingReadme.substring(0, sectionIndex).trim();
|
||||
const afterSection = existingReadme.substring(endIndex);
|
||||
mergedReadme = beforeSection + '\n\n' + readmeAdditions.trim() + '\n\n' + afterSection;
|
||||
} else {
|
||||
// No "Get started" section found, append the template to the existing README
|
||||
mergedReadme = existingReadme.trim() + '\n\n' + readmeAdditions.trim();
|
||||
}
|
||||
|
||||
mergedReadme = mergedReadme.replaceAll('npm run', `${packageManager} run`);
|
||||
|
||||
await fs.writeFile(projectReadmePath, mergedReadme);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
name: Create development builds
|
||||
|
||||
jobs:
|
||||
android_development_build:
|
||||
name: Build Android
|
||||
type: build
|
||||
params:
|
||||
platform: android
|
||||
profile: development
|
||||
ios_device_development_build:
|
||||
name: Build iOS device
|
||||
type: build
|
||||
params:
|
||||
platform: ios
|
||||
profile: development
|
||||
ios_simulator_development_build:
|
||||
name: Build iOS simulator
|
||||
type: build
|
||||
params:
|
||||
platform: ios
|
||||
profile: development-simulator
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Create draft
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
publish_preview_update:
|
||||
name: Publish preview update
|
||||
type: update
|
||||
params:
|
||||
branch: ${{ github.ref_name || 'test' }}
|
||||
deploy_website:
|
||||
name: Deploy website
|
||||
type: deploy
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
name: Deploy to production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
fingerprint:
|
||||
name: Fingerprint
|
||||
type: fingerprint
|
||||
get_android_build:
|
||||
name: Check for existing android build
|
||||
needs: [fingerprint]
|
||||
type: get-build
|
||||
params:
|
||||
fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
|
||||
profile: production
|
||||
get_ios_build:
|
||||
name: Check for existing ios build
|
||||
needs: [fingerprint]
|
||||
type: get-build
|
||||
params:
|
||||
fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
|
||||
profile: production
|
||||
build_android:
|
||||
name: Build Android
|
||||
needs: [get_android_build]
|
||||
if: ${{ !needs.get_android_build.outputs.build_id }}
|
||||
type: build
|
||||
params:
|
||||
platform: android
|
||||
profile: production
|
||||
build_ios:
|
||||
name: Build iOS
|
||||
needs: [get_ios_build]
|
||||
if: ${{ !needs.get_ios_build.outputs.build_id }}
|
||||
type: build
|
||||
params:
|
||||
platform: ios
|
||||
profile: production
|
||||
submit_android_build:
|
||||
name: Submit Android Build
|
||||
needs: [build_android]
|
||||
type: submit
|
||||
params:
|
||||
build_id: ${{ needs.build_android.outputs.build_id }}
|
||||
submit_ios_build:
|
||||
name: Submit iOS Build
|
||||
needs: [build_ios]
|
||||
type: submit
|
||||
params:
|
||||
build_id: ${{ needs.build_ios.outputs.build_id }}
|
||||
publish_android_update:
|
||||
name: Publish Android update
|
||||
needs: [get_android_build]
|
||||
if: ${{ needs.get_android_build.outputs.build_id }}
|
||||
type: update
|
||||
params:
|
||||
branch: production
|
||||
platform: android
|
||||
publish_ios_update:
|
||||
name: Publish iOS update
|
||||
needs: [get_ios_build]
|
||||
if: ${{ needs.get_ios_build.outputs.build_id }}
|
||||
type: update
|
||||
params:
|
||||
branch: production
|
||||
platform: ios
|
||||
@@ -0,0 +1,163 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is an Expo/React Native mobile application. Prioritize mobile-first patterns, performance, and cross-platform compatibility.
|
||||
|
||||
## Documentation Resources
|
||||
|
||||
When working on this project, **always consult the official Expo documentation** available at:
|
||||
|
||||
- **https://docs.expo.dev/llms.txt** - Index of all available documentation files
|
||||
- **https://docs.expo.dev/llms-full.txt** - Complete Expo documentation including Expo Router, Expo Modules API, development process
|
||||
- **https://docs.expo.dev/llms-eas.txt** - Complete EAS (Expo Application Services) documentation
|
||||
- **https://docs.expo.dev/llms-sdk.txt** - Complete Expo SDK documentation
|
||||
- **https://reactnative.dev/docs/getting-started** - Complete React Native documentation
|
||||
|
||||
These documentation files are specifically formatted for AI agents and should be your **primary reference** for:
|
||||
|
||||
- Expo APIs and best practices
|
||||
- Expo Router navigation patterns
|
||||
- EAS Build, Submit, and Update workflows
|
||||
- Expo SDK modules and their usage
|
||||
- Development and deployment processes
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── app/ # Expo Router file-based routing
|
||||
│ ├── (tabs)/ # Tab-based navigation screens
|
||||
│ │ ├── index.tsx # Home screen
|
||||
│ │ ├── explore.tsx # Explore screen
|
||||
│ │ └── _layout.tsx # Tabs layout
|
||||
│ ├── _layout.tsx # Root layout with theme provider
|
||||
│ └── modal.tsx # Modal screen example
|
||||
├── components/ # Reusable React components
|
||||
│ ├── ui/ # UI primitives (IconSymbol, Collapsible)
|
||||
│ └── ... # Feature components (themed, haptic, parallax)
|
||||
├── constants/ # App-wide constants (theme, colors)
|
||||
├── hooks/ # Custom React hooks (color scheme, theme)
|
||||
├── assets/ # Static assets (images, fonts)
|
||||
├── scripts/ # Utility scripts (reset-project)
|
||||
├── .eas/workflows/ # EAS Workflows (CI/CD automation)
|
||||
├── app.json # Expo configuration
|
||||
├── eas.json # EAS Build/Submit configuration
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npx expo start # Start dev server
|
||||
npx expo start --clear # Clear cache and start dev server
|
||||
npx expo install <package> # Install packages with compatible versions
|
||||
npx expo install --check # Check which installed packages need to be updated
|
||||
npx expo install --fix # Automatically update any invalid package versions
|
||||
npm run development-builds # Create development builds (workflow)
|
||||
npm run reset-project # Reset to blank template
|
||||
```
|
||||
|
||||
### Building & Testing
|
||||
|
||||
```bash
|
||||
npx expo doctor # Check project health and dependencies
|
||||
npx expo lint # Run ESLint
|
||||
npm run draft # Publish preview update and website (workflow)
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
npx eas-cli@latest build --platform ios -s # Use EAS to build for iOS platform and submit to App Store
|
||||
npx eas-cli@latest build --platform android -s # Use EAS to build for Android platform and submit to Google Play Store
|
||||
npm run deploy # Deploy to production (workflow)
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Style & Standards
|
||||
|
||||
- **TypeScript First**: Use TypeScript for all new code with strict type checking
|
||||
- **Naming Conventions**: Use meaningful, descriptive names for variables, functions, and components
|
||||
- **Self-Documenting Code**: Write clear, readable code that explains itself; only add comments for complex business logic or design decisions
|
||||
- **React 19 Patterns**: Follow modern React patterns including:
|
||||
- Function components with hooks
|
||||
- Enable React Compiler
|
||||
- Proper dependency arrays in useEffect
|
||||
- Memoization when appropriate (useMemo, useCallback)
|
||||
- Error boundaries for better error handling
|
||||
|
||||
### Navigation & Routing
|
||||
|
||||
- Use **Expo Router** for all navigation
|
||||
- Import `Link`, `router`, and `useLocalSearchParams` from `expo-router`
|
||||
- Docs: https://docs.expo.dev/router/introduction/
|
||||
|
||||
### Recommended Libraries
|
||||
|
||||
- **Navigation**: `expo-router` for navigation
|
||||
- **Images**: `expo-image` for optimized image handling and caching
|
||||
- **Animations**: `react-native-reanimated` for performant animations on native thread
|
||||
- **Gestures**: `react-native-gesture-handler` for native gesture recognition
|
||||
- **Storage**: Use `expo-sqlite` for persistent storage, `expo-sqlite/kv-store` for simple key-value storage
|
||||
|
||||
## Debugging & Development Tools
|
||||
|
||||
### DevTools Integration
|
||||
|
||||
- **React Native DevTools**: Use MCP `open_devtools` command to launch debugging tools
|
||||
- **Network Inspection**: Monitor API calls and network requests in DevTools
|
||||
- **Element Inspector**: Debug component hierarchy and styles
|
||||
- **Performance Profiler**: Identify performance bottlenecks
|
||||
- **Logging**: Use `console.log` for debugging (remove before production), `console.warn` for deprecation notices, `console.error` for actual errors, and implement error boundaries for production error handling
|
||||
|
||||
### Testing & Quality Assurance
|
||||
|
||||
#### Automated Testing with MCP Tools
|
||||
|
||||
Developers can configure the Expo MCP server with the following doc: https://docs.expo.dev/eas/ai/mcp/
|
||||
|
||||
- **Component Testing**: Add `testID` props to components for automation
|
||||
- **Visual Testing**: Use MCP `automation_take_screenshot` to verify UI appearance
|
||||
- **Interaction Testing**: Use MCP `automation_tap_by_testid` to simulate user interactions
|
||||
- **View Verification**: Use MCP `automation_find_view_by_testid` to validate component rendering
|
||||
|
||||
## EAS Workflows CI/CD
|
||||
|
||||
This project is pre-configured with **EAS Workflows** for automating development and release processes. Workflows are defined in `.eas/workflows/` directory.
|
||||
|
||||
When working with EAS Workflows, **always refer to**:
|
||||
|
||||
- https://docs.expo.dev/eas/workflows/ for workflow examples
|
||||
- The `.eas/workflows/` directory for existing workflow configurations
|
||||
- You can check that a workflow YAML is valid using the workflows schema: https://exp.host/--/api/v2/workflows/schema
|
||||
|
||||
### Build Profiles (eas.json)
|
||||
|
||||
- **development**: Development builds with dev client
|
||||
- **development-simulator**: Development builds for iOS simulator
|
||||
- **preview**: Internal distribution preview builds
|
||||
- **production**: Production builds with auto-increment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Expo Go Errors & Development Builds
|
||||
|
||||
If there are errors in **Expo Go** or the project is not running, create a **development build**. **Expo Go** is a sandbox environment with a limited set of native modules. To create development builds, run `eas build:dev`. Additionally, after installing new packages or adding config plugins, new development builds are often required.
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
When working on this project:
|
||||
|
||||
1. **Always start by consulting the appropriate documentation**:
|
||||
|
||||
- For general Expo questions: https://docs.expo.dev/llms-full.txt
|
||||
- For EAS/deployment questions: https://docs.expo.dev/llms-eas.txt
|
||||
- For SDK/API questions: https://docs.expo.dev/llms-sdk.txt
|
||||
|
||||
2. **Understand before implementing**: Read the relevant docs section before writing code
|
||||
|
||||
3. **Follow existing patterns**: Look at existing components and screens for patterns to follow
|
||||
@@ -0,0 +1,9 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
**Note**: This project uses `AGENTS.md` files for detailed guidance.
|
||||
|
||||
## Primary Reference
|
||||
|
||||
Please see `AGENTS.md` in this same directory for the main project documentation and guidance.
|
||||
@@ -0,0 +1,36 @@
|
||||
## Get started
|
||||
|
||||
To start the app, in your terminal run:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in:
|
||||
|
||||
- [a development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [an Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [an iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Workflows
|
||||
|
||||
This project is configured to use [EAS Workflows](https://docs.expo.dev/eas/workflows/get-started/) to automate some development and release processes. These commands are set up in [`package.json`](./package.json) and can be run using NPM scripts in your terminal.
|
||||
|
||||
### Previews
|
||||
|
||||
Run `npm run preview` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
|
||||
|
||||
### Development Builds
|
||||
|
||||
Run `npm run development-builds` to [create a development build](https://docs.expo.dev/eas/workflows/examples/create-development-builds/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/create-development-builds/#prerequisites) to ensure you have the correct emulator setup on your machine.
|
||||
|
||||
### Production Deployments
|
||||
|
||||
Run `npm run deploy` to [deploy to production](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/). Note - you'll need to follow the [Prerequisites](https://docs.expo.dev/eas/workflows/examples/deploy-to-production/#prerequisites) to ensure you're set up to submit to the Apple and Google stores.
|
||||
|
||||
## Hosting
|
||||
|
||||
Expo offers hosting for websites and API functions via EAS Hosting. See the [Getting Started](https://docs.expo.dev/eas/hosting/get-started/) guide to learn more.
|
||||
@@ -0,0 +1,20 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Formats a directory path for display.
|
||||
* If the directory is within the current working directory, returns a relative path.
|
||||
* Otherwise, returns the absolute path.
|
||||
*/
|
||||
export function printDirectory(directory: string): string {
|
||||
const cwd = process.cwd();
|
||||
const absoluteDir = path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
|
||||
|
||||
const relativePath = path.relative(cwd, absoluteDir);
|
||||
|
||||
// If the relative path doesn't start with '..' it means it's within or at the cwd
|
||||
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
||||
return relativePath !== '' ? `./${relativePath}` : '.';
|
||||
}
|
||||
|
||||
return absoluteDir;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { BuildProfile, EasJsonAccessor, EasJsonUtils, Platform } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, IosBuildProfile } from '@expo/eas-json/build/build/types';
|
||||
|
||||
import BuildConfigure from '../../commands/build/configure';
|
||||
import UpdateConfigure from '../../commands/update/configure';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
export async function buildProfileNamesFromProjectAsync(projectDir: string): Promise<Set<string>> {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
|
||||
const buildProfileNames = new Set(
|
||||
easJsonAccessor && (await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor))
|
||||
);
|
||||
return buildProfileNames;
|
||||
}
|
||||
export async function getBuildProfileAsync(
|
||||
projectDir: string,
|
||||
platform: Platform,
|
||||
profileName: string
|
||||
): Promise<BuildProfile<Platform>> {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
platform,
|
||||
profileName
|
||||
);
|
||||
return buildProfile;
|
||||
}
|
||||
export async function buildProfilesFromProjectAsync(
|
||||
projectDir: string
|
||||
): Promise<Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }>> {
|
||||
const buildProfileNames = await buildProfileNamesFromProjectAsync(projectDir);
|
||||
const buildProfiles: Map<string, { android: AndroidBuildProfile; ios: IosBuildProfile }> =
|
||||
new Map();
|
||||
for (const profileName of buildProfileNames) {
|
||||
const iosBuildProfile = (await getBuildProfileAsync(
|
||||
projectDir,
|
||||
Platform.IOS,
|
||||
profileName
|
||||
)) as IosBuildProfile;
|
||||
const androidBuildProfile = (await getBuildProfileAsync(
|
||||
projectDir,
|
||||
Platform.ANDROID,
|
||||
profileName
|
||||
)) as AndroidBuildProfile;
|
||||
buildProfiles.set(profileName, { android: androidBuildProfile, ios: iosBuildProfile });
|
||||
}
|
||||
return buildProfiles;
|
||||
}
|
||||
export function isBuildProfileForDevelopment(
|
||||
buildProfile: BuildProfile<Platform>,
|
||||
platform: Platform
|
||||
): boolean {
|
||||
if (buildProfile.developmentClient) {
|
||||
return true;
|
||||
}
|
||||
if (platform === Platform.ANDROID) {
|
||||
return (buildProfile as BuildProfile<Platform.ANDROID>).gradleCommand === ':app:assembleDebug';
|
||||
}
|
||||
if (platform === Platform.IOS) {
|
||||
return (buildProfile as BuildProfile<Platform.IOS>).buildConfiguration === 'Debug';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function isIosBuildProfileForSimulator(buildProfile: BuildProfile<Platform.IOS>): boolean {
|
||||
return buildProfile.simulator ?? false;
|
||||
}
|
||||
export async function addAndroidDevelopmentBuildProfileToEasJsonAsync(
|
||||
projectDir: string,
|
||||
buildProfileName: string
|
||||
): Promise<void> {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
easJsonRawObject.build = {
|
||||
...easJsonRawObject.build,
|
||||
[buildProfileName]: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
},
|
||||
};
|
||||
return easJsonRawObject;
|
||||
});
|
||||
await easJsonAccessor.writeAsync();
|
||||
}
|
||||
export async function addIosDevelopmentBuildProfileToEasJsonAsync(
|
||||
projectDir: string,
|
||||
buildProfileName: string,
|
||||
simulator: boolean
|
||||
): Promise<void> {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
easJsonRawObject.build = {
|
||||
...easJsonRawObject.build,
|
||||
[buildProfileName]: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
simulator,
|
||||
},
|
||||
},
|
||||
};
|
||||
return easJsonRawObject;
|
||||
});
|
||||
await easJsonAccessor.writeAsync();
|
||||
}
|
||||
|
||||
export async function addProductionBuildProfileToEasJsonIfNeededAsync(
|
||||
projectDir: string
|
||||
): Promise<boolean> {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
let profileAdded = false;
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
if (!easJsonRawObject.build?.production) {
|
||||
profileAdded = true;
|
||||
easJsonRawObject.build = {
|
||||
...(easJsonRawObject.build ?? {}),
|
||||
production: {},
|
||||
};
|
||||
// Also add the profile to submit
|
||||
easJsonRawObject.submit = {
|
||||
...(easJsonRawObject.submit ?? {}),
|
||||
production: {},
|
||||
};
|
||||
}
|
||||
return easJsonRawObject;
|
||||
});
|
||||
if (profileAdded) {
|
||||
Log.log('Added missing production build profile to eas.json');
|
||||
}
|
||||
await easJsonAccessor.writeAsync();
|
||||
return profileAdded;
|
||||
}
|
||||
|
||||
export async function hasBuildConfigureBeenRunAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
}: {
|
||||
projectDir: string;
|
||||
expoConfig: ExpoConfig;
|
||||
}): Promise<boolean> {
|
||||
// Is there a project ID in the Expo config?
|
||||
if (!expoConfig.extra?.eas?.projectId) {
|
||||
return false;
|
||||
}
|
||||
// Is there an eas.json?
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
try {
|
||||
await easJsonAccessor.readAsync();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function hasUpdateConfigureBeenRunAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
}: {
|
||||
projectDir: string;
|
||||
expoConfig: ExpoConfig;
|
||||
}): Promise<boolean> {
|
||||
// Does the Expo config have an updates URL?
|
||||
if (!expoConfig.updates?.url) {
|
||||
return false;
|
||||
}
|
||||
// Does at least one build profile have a channel?
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
try {
|
||||
const easJson = await easJsonAccessor.readAsync();
|
||||
return Object.values(easJson.build ?? {}).some(buildProfile => !!buildProfile.channel);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs update:configure if needed. Returns a boolean (proceed with workflow creation, or not)
|
||||
*/
|
||||
|
||||
export async function runUpdateConfigureIfNeededAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
}: {
|
||||
projectDir: string;
|
||||
expoConfig: ExpoConfig;
|
||||
}): Promise<boolean> {
|
||||
if (
|
||||
await hasUpdateConfigureBeenRunAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const nextStep = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'nextStep',
|
||||
message:
|
||||
'You have chosen to create a workflow that requires EAS Update configuration. What would you like to do?',
|
||||
choices: [
|
||||
{ title: 'Configure EAS Update and then proceed', value: 'configure' },
|
||||
{ title: 'EAS Update is already configured, proceed', value: 'proceed' },
|
||||
{ title: 'Choose a different workflow template', value: 'repeat' },
|
||||
],
|
||||
})
|
||||
).nextStep;
|
||||
switch (nextStep) {
|
||||
case 'configure':
|
||||
Log.newLine();
|
||||
await UpdateConfigure.run([]);
|
||||
return true;
|
||||
case 'proceed':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Runs build:configure if needed. Returns a boolean (proceed with workflow creation, or not)
|
||||
*/
|
||||
export async function runBuildConfigureIfNeededAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
}: {
|
||||
projectDir: string;
|
||||
expoConfig: ExpoConfig;
|
||||
}): Promise<boolean> {
|
||||
if (
|
||||
await hasBuildConfigureBeenRunAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const nextStep = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'nextStep',
|
||||
message:
|
||||
'You have chosen to create a workflow that requires EAS Build configuration. What would you like to do?',
|
||||
choices: [
|
||||
{ title: 'Configure EAS Build and then proceed', value: 'configure' },
|
||||
{ title: 'EAS Build is already configured, proceed', value: 'proceed' },
|
||||
{ title: 'Choose a different workflow template', value: 'repeat' },
|
||||
],
|
||||
})
|
||||
).nextStep;
|
||||
switch (nextStep) {
|
||||
case 'configure':
|
||||
Log.newLine();
|
||||
await BuildConfigure.run(['-p', 'all']);
|
||||
return true;
|
||||
case 'proceed':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
addAndroidDevelopmentBuildProfileToEasJsonAsync,
|
||||
addIosDevelopmentBuildProfileToEasJsonAsync,
|
||||
addProductionBuildProfileToEasJsonIfNeededAsync,
|
||||
buildProfilesFromProjectAsync,
|
||||
hasBuildConfigureBeenRunAsync,
|
||||
hasUpdateConfigureBeenRunAsync,
|
||||
isBuildProfileForDevelopment,
|
||||
isIosBuildProfileForSimulator,
|
||||
} from './buildProfileUtils';
|
||||
import Log, { link } from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
|
||||
export enum WorkflowStarterName {
|
||||
BUILD = 'build',
|
||||
UPDATE = 'update',
|
||||
CUSTOM = 'custom',
|
||||
DEPLOY = 'deploy',
|
||||
}
|
||||
|
||||
export type WorkflowStarter = {
|
||||
name: WorkflowStarterName;
|
||||
displayName: string;
|
||||
defaultFileName: string;
|
||||
template: any;
|
||||
header: string;
|
||||
nextSteps?: string[];
|
||||
};
|
||||
|
||||
const createdByEASCLI = `# Created by EAS CLI v${easCliVersion}`;
|
||||
|
||||
const CUSTOM_TEMPLATE = {
|
||||
name: 'Custom workflow',
|
||||
on: {
|
||||
push: {
|
||||
branches: ['main'],
|
||||
},
|
||||
},
|
||||
jobs: {
|
||||
custom_build: {
|
||||
name: 'Custom job',
|
||||
steps: [
|
||||
{
|
||||
uses: 'eas/checkout',
|
||||
},
|
||||
{
|
||||
name: 'Hello World',
|
||||
id: 'hello_world',
|
||||
run: '# Custom script\necho "Hello, World"\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const CUSTOM_TEMPLATE_HEADER = `
|
||||
# Custom job
|
||||
#
|
||||
# This workflow shows how to write custom jobs.
|
||||
# It contains a predefined workflow step and a shell command to print "Hello, World!".
|
||||
#
|
||||
# Key features:
|
||||
# - Triggers on pushes to the main branch.
|
||||
# (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
|
||||
# - Can be triggered manually with eas workflow:run custom.yml
|
||||
# - Runs eas/checkout then a custom "echo" command
|
||||
#
|
||||
# For detailed documentation on workflow syntax and available step types, visit:
|
||||
# https://docs.expo.dev/eas/workflows/syntax/#jobsjob_idstepsstepuses
|
||||
#
|
||||
${createdByEASCLI}
|
||||
#
|
||||
`;
|
||||
|
||||
const BUILD_TEMPLATE = {
|
||||
name: 'Create development builds',
|
||||
on: {
|
||||
push: {
|
||||
branches: ['main'],
|
||||
},
|
||||
},
|
||||
jobs: {},
|
||||
};
|
||||
|
||||
const BUILD_TEMPLATE_HEADER = `
|
||||
# Create development builds
|
||||
#
|
||||
# This workflow shows how to create development builds.
|
||||
#
|
||||
# Key features:
|
||||
# - Can be triggered manually with eas workflow:run create-development-builds.yml
|
||||
# - Runs the pre-packaged build job to create Android and iOS development builds
|
||||
# for Android emulators, Android and iOS devices, and iOS simulators
|
||||
#
|
||||
# For a detailed guide on using this workflow, visit:
|
||||
# https://docs.expo.dev/develop/development-builds/introduction/
|
||||
#
|
||||
${createdByEASCLI}
|
||||
#
|
||||
`;
|
||||
|
||||
const PUBLISH_UPDATE_TEMPLATE = {
|
||||
name: 'Publish preview update',
|
||||
on: {
|
||||
push: {
|
||||
branches: ['*'],
|
||||
},
|
||||
},
|
||||
jobs: {
|
||||
publish_preview_update: {
|
||||
name: 'Publish preview update',
|
||||
type: 'update',
|
||||
params: {
|
||||
branch: '${{ github.ref_name || "test" }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const PUBLISH_UPDATE_TEMPLATE_HEADER = `
|
||||
# Publish preview update
|
||||
#
|
||||
# This workflow shows how to publish preview updates.
|
||||
# Learn more: https://docs.expo.dev/review/share-previews-with-your-team/
|
||||
#
|
||||
# Key features:
|
||||
# - Triggers on pushes to all branches
|
||||
# (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
|
||||
# - Can be triggered manually with eas workflow:run publish-preview-update.yml
|
||||
# - Runs the pre-packaged update job
|
||||
#
|
||||
# For a detailed guide on using this workflow, visit:
|
||||
# https://docs.expo.dev/eas/workflows/examples/publish-preview-update/
|
||||
#
|
||||
${createdByEASCLI}
|
||||
#
|
||||
`;
|
||||
|
||||
const DEPLOY_TEMPLATE = {
|
||||
name: 'Deploy to production',
|
||||
on: {
|
||||
push: {
|
||||
branches: ['main'],
|
||||
},
|
||||
},
|
||||
jobs: {
|
||||
fingerprint: {
|
||||
name: 'Fingerprint',
|
||||
type: 'fingerprint',
|
||||
},
|
||||
get_android_build: {
|
||||
name: 'Check for existing android build',
|
||||
needs: ['fingerprint'],
|
||||
type: 'get-build',
|
||||
params: {
|
||||
fingerprint_hash: '${{ needs.fingerprint.outputs.android_fingerprint_hash }}',
|
||||
profile: 'production',
|
||||
},
|
||||
},
|
||||
get_ios_build: {
|
||||
name: 'Check for existing ios build',
|
||||
needs: ['fingerprint'],
|
||||
type: 'get-build',
|
||||
params: {
|
||||
fingerprint_hash: '${{ needs.fingerprint.outputs.ios_fingerprint_hash }}',
|
||||
profile: 'production',
|
||||
},
|
||||
},
|
||||
build_android: {
|
||||
name: 'Build Android',
|
||||
needs: ['get_android_build'],
|
||||
if: '${{ !needs.get_android_build.outputs.build_id }}',
|
||||
type: 'build',
|
||||
params: {
|
||||
platform: 'android',
|
||||
profile: 'production',
|
||||
},
|
||||
},
|
||||
build_ios: {
|
||||
name: 'Build iOS',
|
||||
needs: ['get_ios_build'],
|
||||
if: '${{ !needs.get_ios_build.outputs.build_id }}',
|
||||
type: 'build',
|
||||
params: {
|
||||
platform: 'ios',
|
||||
profile: 'production',
|
||||
},
|
||||
},
|
||||
submit_android_build: {
|
||||
name: 'Submit Android Build',
|
||||
needs: ['build_android'],
|
||||
type: 'submit',
|
||||
params: {
|
||||
build_id: '${{ needs.build_android.outputs.build_id }}',
|
||||
},
|
||||
},
|
||||
submit_ios_build: {
|
||||
name: 'Submit iOS Build',
|
||||
needs: ['build_ios'],
|
||||
type: 'submit',
|
||||
params: {
|
||||
build_id: '${{ needs.build_ios.outputs.build_id }}',
|
||||
},
|
||||
},
|
||||
publish_android_update: {
|
||||
name: 'Publish Android update',
|
||||
needs: ['get_android_build'],
|
||||
if: '${{ needs.get_android_build.outputs.build_id }}',
|
||||
type: 'update',
|
||||
params: {
|
||||
branch: 'production',
|
||||
platform: 'android',
|
||||
},
|
||||
},
|
||||
publish_ios_update: {
|
||||
name: 'Publish iOS update',
|
||||
needs: ['get_ios_build'],
|
||||
if: '${{ needs.get_ios_build.outputs.build_id }}',
|
||||
type: 'update',
|
||||
params: {
|
||||
branch: 'production',
|
||||
platform: 'ios',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const DEPLOY_TEMPLATE_HEADER = `
|
||||
# Deploy to production
|
||||
#
|
||||
# This workflow shows how to build and submit to app stores.
|
||||
#
|
||||
# Key features:
|
||||
# - Triggers on pushes to "main" (Requires linked GitHub account: https://expo.dev/accounts/[account]/settings/github)
|
||||
# - Can be triggered manually with eas workflow:run deploy-to-production.yml
|
||||
# - Creates builds and submits them to app stores when native changes are detected, otherwise sends an over-the-air update.
|
||||
#
|
||||
# For a detailed guide on using this workflow, visit:
|
||||
# https://docs.expo.dev/eas/workflows/examples/deploy-to-production/
|
||||
`;
|
||||
|
||||
function nextStepForDeviceBuildProfile(buildProfileName: string): string {
|
||||
return `A build job in this workflow uses the build profile "${buildProfileName}"
|
||||
to build your app for real devices, and may require credentials.
|
||||
You can configure your credentials with the command "eas credentials".
|
||||
For more information, please see ${link('https://docs.expo.dev/app-signing/app-credentials/')}.
|
||||
`
|
||||
.trim()
|
||||
.trimStart();
|
||||
}
|
||||
|
||||
function nextStepForAppSubmission(): string {
|
||||
return `This workflow includes a job to submit your app to app stores.
|
||||
You will need to configure your App Store/Play Store credentials
|
||||
before you can run this workflow.
|
||||
Please see ${link('https://docs.expo.dev/deploy/submit-to-app-stores/')}.
|
||||
`
|
||||
.trim()
|
||||
.trimStart();
|
||||
}
|
||||
|
||||
export function howToRunWorkflow(
|
||||
workflowFileName: string,
|
||||
workflowStarter: WorkflowStarter
|
||||
): string {
|
||||
const lines = [
|
||||
`To run this workflow manually, use the command "eas workflow:run .eas/workflows/${workflowFileName}".`,
|
||||
];
|
||||
if (workflowStarter.template?.on?.push?.branches?.length > 0) {
|
||||
const branches = workflowStarter.template.on.push.branches;
|
||||
if (branches.length === 1 && branches[0] === '*') {
|
||||
lines.push(
|
||||
'This workflow is also configured to run automatically when code is pushed to any branch.'
|
||||
);
|
||||
} else if (branches.length === 1) {
|
||||
lines.push(
|
||||
`This workflow is also configured to run automatically when code is pushed to the "${branches[0]}" branch.`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`This workflow is also configured to run automatically when code is pushed to the following branches: ${branches.join(
|
||||
', '
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export const workflowStarters: WorkflowStarter[] = [
|
||||
{
|
||||
displayName: 'Custom',
|
||||
name: WorkflowStarterName.CUSTOM,
|
||||
defaultFileName: 'custom.yml',
|
||||
template: CUSTOM_TEMPLATE,
|
||||
header: CUSTOM_TEMPLATE_HEADER,
|
||||
},
|
||||
{
|
||||
displayName: 'Create development builds',
|
||||
name: WorkflowStarterName.BUILD,
|
||||
defaultFileName: 'create-development-builds.yml',
|
||||
template: BUILD_TEMPLATE,
|
||||
header: BUILD_TEMPLATE_HEADER,
|
||||
},
|
||||
{
|
||||
displayName: 'Publish updates',
|
||||
name: WorkflowStarterName.UPDATE,
|
||||
defaultFileName: 'publish-updates.yml',
|
||||
template: PUBLISH_UPDATE_TEMPLATE,
|
||||
header: PUBLISH_UPDATE_TEMPLATE_HEADER,
|
||||
},
|
||||
{
|
||||
displayName: 'Deploy to production',
|
||||
name: WorkflowStarterName.DEPLOY,
|
||||
defaultFileName: 'deploy-to-production.yml',
|
||||
template: DEPLOY_TEMPLATE,
|
||||
header: DEPLOY_TEMPLATE_HEADER,
|
||||
},
|
||||
];
|
||||
|
||||
export async function addBuildJobsToDevelopmentBuildTemplateAsync(
|
||||
projectDir: string,
|
||||
workflowStarter: WorkflowStarter
|
||||
): Promise<WorkflowStarter> {
|
||||
const buildProfiles = await buildProfilesFromProjectAsync(projectDir);
|
||||
// android_development_build
|
||||
|
||||
const nextSteps: Set<string> = new Set();
|
||||
|
||||
let androidDevelopmentBuildProfileName: string | null = null;
|
||||
for (const profileName of buildProfiles.keys()) {
|
||||
const profile = buildProfiles.get(profileName)?.android;
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
if (isBuildProfileForDevelopment(profile, Platform.ANDROID)) {
|
||||
androidDevelopmentBuildProfileName = profileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!androidDevelopmentBuildProfileName) {
|
||||
Log.warn(
|
||||
'This workflow requires an Android development build profile in your eas.json, but none were found.'
|
||||
);
|
||||
const add = await confirmAsync({
|
||||
message: 'Do you want to add an Android development build profile?',
|
||||
initial: false,
|
||||
});
|
||||
if (add) {
|
||||
let androidDevelopmentBuildProfileName = 'android_development';
|
||||
while (buildProfiles.has(androidDevelopmentBuildProfileName)) {
|
||||
androidDevelopmentBuildProfileName = `${androidDevelopmentBuildProfileName}_1`;
|
||||
}
|
||||
await addAndroidDevelopmentBuildProfileToEasJsonAsync(
|
||||
projectDir,
|
||||
androidDevelopmentBuildProfileName
|
||||
);
|
||||
} else {
|
||||
Log.log('Skipping Android development build job...');
|
||||
}
|
||||
}
|
||||
if (androidDevelopmentBuildProfileName) {
|
||||
Log.log(`Using Android development build profile: ${androidDevelopmentBuildProfileName}`);
|
||||
workflowStarter.template.jobs.android_development_build = {
|
||||
name: `Build ${androidDevelopmentBuildProfileName} for android`,
|
||||
type: 'build',
|
||||
params: {
|
||||
profile: androidDevelopmentBuildProfileName,
|
||||
platform: 'android',
|
||||
},
|
||||
};
|
||||
nextSteps.add(nextStepForDeviceBuildProfile(androidDevelopmentBuildProfileName));
|
||||
}
|
||||
|
||||
// ios_simulator_development_build
|
||||
let iosSimulatorDevelopmentBuildProfileName: string | null = null;
|
||||
for (const profileName of buildProfiles.keys()) {
|
||||
const profile = buildProfiles.get(profileName)?.ios;
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
isBuildProfileForDevelopment(profile, Platform.IOS) &&
|
||||
isIosBuildProfileForSimulator(profile)
|
||||
) {
|
||||
iosSimulatorDevelopmentBuildProfileName = profileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!iosSimulatorDevelopmentBuildProfileName) {
|
||||
Log.warn(
|
||||
'This workflow requires an iOS simulator development build profile in your eas.json, but none were found.'
|
||||
);
|
||||
const add = await confirmAsync({
|
||||
message: 'Do you want to add an iOS simulator development build profile?',
|
||||
initial: false,
|
||||
});
|
||||
if (add) {
|
||||
let iosSimulatorDevelopmentBuildProfileName = 'ios_simulator_development';
|
||||
while (buildProfiles.has(iosSimulatorDevelopmentBuildProfileName)) {
|
||||
iosSimulatorDevelopmentBuildProfileName = `${iosSimulatorDevelopmentBuildProfileName}_1`;
|
||||
}
|
||||
await addIosDevelopmentBuildProfileToEasJsonAsync(
|
||||
projectDir,
|
||||
iosSimulatorDevelopmentBuildProfileName,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
Log.log('Skipping iOS simulator development build job...');
|
||||
}
|
||||
}
|
||||
if (iosSimulatorDevelopmentBuildProfileName) {
|
||||
Log.log(
|
||||
`Using iOS simulator development build profile: ${iosSimulatorDevelopmentBuildProfileName}`
|
||||
);
|
||||
workflowStarter.template.jobs.ios_simulator_development_build = {
|
||||
name: `Build ${iosSimulatorDevelopmentBuildProfileName} for iOS simulator`,
|
||||
type: 'build',
|
||||
params: {
|
||||
profile: iosSimulatorDevelopmentBuildProfileName,
|
||||
platform: 'ios',
|
||||
},
|
||||
};
|
||||
}
|
||||
// ios_device_development_build
|
||||
let iosDeviceDevelopmentBuildProfileName: string | null = null;
|
||||
for (const profileName of buildProfiles.keys()) {
|
||||
const profile = buildProfiles.get(profileName)?.ios;
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
isBuildProfileForDevelopment(profile, Platform.IOS) &&
|
||||
!isIosBuildProfileForSimulator(profile)
|
||||
) {
|
||||
iosDeviceDevelopmentBuildProfileName = profileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!iosDeviceDevelopmentBuildProfileName) {
|
||||
Log.warn(
|
||||
'This workflow requires an iOS device development build profile in your eas.json, but none were found.'
|
||||
);
|
||||
const add = await confirmAsync({
|
||||
message: 'Do you want to add an iOS device development build profile?',
|
||||
initial: false,
|
||||
});
|
||||
if (add) {
|
||||
let iosDeviceDevelopmentBuildProfileName = 'ios_device_development';
|
||||
while (buildProfiles.has(iosDeviceDevelopmentBuildProfileName)) {
|
||||
iosDeviceDevelopmentBuildProfileName = `${iosDeviceDevelopmentBuildProfileName}_1`;
|
||||
}
|
||||
await addIosDevelopmentBuildProfileToEasJsonAsync(
|
||||
projectDir,
|
||||
iosDeviceDevelopmentBuildProfileName,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
Log.log('Skipping iOS device development build job...');
|
||||
}
|
||||
}
|
||||
if (iosDeviceDevelopmentBuildProfileName) {
|
||||
Log.log(`Using iOS device development build profile: ${iosDeviceDevelopmentBuildProfileName}`);
|
||||
workflowStarter.template.jobs.ios_development_build = {
|
||||
name: `Build ${iosDeviceDevelopmentBuildProfileName} for iOS simulator`,
|
||||
type: 'build',
|
||||
params: {
|
||||
profile: iosDeviceDevelopmentBuildProfileName,
|
||||
platform: 'ios',
|
||||
},
|
||||
};
|
||||
nextSteps.add(nextStepForDeviceBuildProfile(iosDeviceDevelopmentBuildProfileName));
|
||||
}
|
||||
workflowStarter.nextSteps = [...nextSteps];
|
||||
return workflowStarter;
|
||||
}
|
||||
|
||||
export async function ensureProductionBuildProfileExistsAsync(
|
||||
projectDir: string,
|
||||
workflowStarter: WorkflowStarter
|
||||
): Promise<WorkflowStarter> {
|
||||
await addProductionBuildProfileToEasJsonIfNeededAsync(projectDir);
|
||||
workflowStarter.nextSteps = [
|
||||
nextStepForDeviceBuildProfile('production'),
|
||||
nextStepForAppSubmission(),
|
||||
];
|
||||
return workflowStarter;
|
||||
}
|
||||
|
||||
export async function customizeTemplateIfNeededAsync(
|
||||
workflowStarter: WorkflowStarter,
|
||||
projectDir: string,
|
||||
expoConfig: ExpoConfig
|
||||
): Promise<any> {
|
||||
// Ensure EAS Build is configured
|
||||
switch (workflowStarter.name) {
|
||||
case WorkflowStarterName.BUILD:
|
||||
case WorkflowStarterName.DEPLOY:
|
||||
case WorkflowStarterName.UPDATE:
|
||||
if (!(await hasBuildConfigureBeenRunAsync({ projectDir, expoConfig }))) {
|
||||
throw new Error(
|
||||
'EAS Build is not configured for this project. Please run "eas build:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Ensure EAS Update is configured
|
||||
switch (workflowStarter.name) {
|
||||
case WorkflowStarterName.DEPLOY:
|
||||
case WorkflowStarterName.UPDATE:
|
||||
if (!(await hasUpdateConfigureBeenRunAsync({ projectDir, expoConfig }))) {
|
||||
throw new Error(
|
||||
'EAS Update is not configured for this project. Please run "eas update:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Customize template
|
||||
switch (workflowStarter.name) {
|
||||
case WorkflowStarterName.BUILD:
|
||||
Log.debug('Adding build jobs to template...');
|
||||
return await addBuildJobsToDevelopmentBuildTemplateAsync(projectDir, workflowStarter);
|
||||
case WorkflowStarterName.DEPLOY:
|
||||
return await ensureProductionBuildProfileExistsAsync(projectDir, workflowStarter);
|
||||
default:
|
||||
return workflowStarter;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { WorkflowJobResult } from './types';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
// This function is in a separate module for testing purposes
|
||||
|
||||
export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<string | null> {
|
||||
export async function fetchRawLogsForCustomJobAsync(
|
||||
job: WorkflowJobResult
|
||||
): Promise<string | null> {
|
||||
const firstLogFileUrl = job.turtleJobRun?.logFileUrls?.[0];
|
||||
if (!firstLogFileUrl) {
|
||||
return null;
|
||||
@@ -13,3 +17,25 @@ export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<s
|
||||
const rawLogs = await response.text();
|
||||
return rawLogs;
|
||||
}
|
||||
|
||||
export async function fetchRawLogsForBuildJobAsync(
|
||||
state: { graphqlClient: ExpoGraphqlClient },
|
||||
job: WorkflowJobResult
|
||||
): Promise<string | null> {
|
||||
const buildId = job.outputs?.build_id;
|
||||
if (!buildId) {
|
||||
return null;
|
||||
}
|
||||
const buildFragment = await BuildQuery.byIdAsync(state.graphqlClient, buildId, {
|
||||
useCache: false,
|
||||
});
|
||||
const firstLogFileUrl = buildFragment.logFiles?.[0];
|
||||
if (!firstLogFileUrl) {
|
||||
return null;
|
||||
}
|
||||
const response = await fetch(firstLogFileUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
const rawLogs = await response.text();
|
||||
return rawLogs;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DefaultEnvironment } from '../../build/utils/environment';
|
||||
import { booleanLike, stringLike } from '../../credentials/ios/types';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
const inputExtraProperties = {
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
required: booleanLike.default(false).optional().describe('Whether the input is required.'),
|
||||
};
|
||||
|
||||
// Adapted from the input definition on the server (https://github.com/expo/universe/pull/21950)
|
||||
export const WorkflowDispatchInputZ = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('string').default('string').optional(),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('boolean'),
|
||||
default: booleanLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('choice'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('environment'),
|
||||
default: z
|
||||
.enum(Object.values(DefaultEnvironment))
|
||||
.optional()
|
||||
.describe('Default value for the input'),
|
||||
}),
|
||||
]);
|
||||
export function parseInputs(inputFlags: string[]): Record<string, string> {
|
||||
const inputs: Record<string, string> = {};
|
||||
|
||||
for (const inputFlag of inputFlags) {
|
||||
const equalIndex = inputFlag.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
|
||||
}
|
||||
|
||||
const key = inputFlag.substring(0, equalIndex);
|
||||
const value = inputFlag.substring(equalIndex + 1);
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
|
||||
}
|
||||
|
||||
inputs[key] = value;
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export function parseJsonInputs(jsonString: string): Record<string, string> {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('JSON input must be an object.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input.`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
.parse(parsed?.on?.workflow_dispatch?.inputs);
|
||||
} catch (error) {
|
||||
Log.warn('Failed to parse workflow inputs from YAML:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
export async function maybePromptForMissingInputsAsync({
|
||||
inputSpecs,
|
||||
inputs,
|
||||
}: {
|
||||
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
|
||||
inputs: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
|
||||
|
||||
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
|
||||
|
||||
if (missingRequiredInputs.length === 0) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Some required inputs are missing. Please provide them:');
|
||||
|
||||
const nextInputs = { ...inputs };
|
||||
|
||||
for (const [key, spec] of missingRequiredInputs) {
|
||||
const value = await promptForMissingInputAsync({ key, spec });
|
||||
nextInputs[key] = value;
|
||||
}
|
||||
|
||||
return nextInputs;
|
||||
}
|
||||
|
||||
async function promptForMissingInputAsync({
|
||||
key,
|
||||
spec,
|
||||
}: {
|
||||
key: string;
|
||||
spec: z.infer<typeof WorkflowDispatchInputZ>;
|
||||
}): Promise<unknown> {
|
||||
const message = spec.description ? `${key} (${spec.description})` : key;
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'number',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: number) => {
|
||||
if (isNaN(val)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'choice': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'value',
|
||||
message,
|
||||
choices: spec.options.map(option => ({
|
||||
title: option,
|
||||
value: option,
|
||||
})),
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'environment':
|
||||
default: {
|
||||
const { value } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: string) => {
|
||||
if (spec.required && (!val || val.trim() === '')) {
|
||||
return 'This field is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
choiceFromWorkflowJob,
|
||||
choiceFromWorkflowRun,
|
||||
choicesFromWorkflowLogs,
|
||||
processLogsFromJobAsync,
|
||||
fetchAndProcessLogsFromJobAsync,
|
||||
processWorkflowRuns,
|
||||
} from './utils';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
@@ -268,7 +268,7 @@ export const workflowStepSelectionAction: WorkflowCommandSelectionAction = async
|
||||
if (!job) {
|
||||
return moveToWorkflowSelectionErrorState(prevState, 'No job found');
|
||||
}
|
||||
const logs = await processLogsFromJobAsync(job);
|
||||
const logs = await fetchAndProcessLogsFromJobAsync(prevState, job);
|
||||
if (!logs) {
|
||||
return moveToWorkflowSelectionErrorState(prevState, 'No logs found');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkflowJobByIdQuery, WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
|
||||
|
||||
/*
|
||||
* Utility types for workflow commands
|
||||
* Utility types and Zod definitions for workflow commands
|
||||
*/
|
||||
|
||||
export enum WorkflowTriggerType {
|
||||
@@ -42,5 +42,12 @@ export type WorkflowRunWithJobsResult = WorkflowRunResult & {
|
||||
logs?: string;
|
||||
};
|
||||
|
||||
export type WorkflowLogLine = { time: string; msg: string; result?: string; marker?: string };
|
||||
export type WorkflowLogLine = {
|
||||
time: string;
|
||||
msg: string;
|
||||
result?: string;
|
||||
marker?: string;
|
||||
err?: any;
|
||||
};
|
||||
|
||||
export type WorkflowLogs = Map<string, WorkflowLogLine[]>;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { fetchRawLogsForJobAsync } from './fetchLogs';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { fetchRawLogsForBuildJobAsync, fetchRawLogsForCustomJobAsync } from './fetchLogs';
|
||||
import {
|
||||
WorkflowJobResult,
|
||||
WorkflowLogLine,
|
||||
@@ -7,11 +9,17 @@ import {
|
||||
WorkflowTriggerType,
|
||||
} from './types';
|
||||
import {
|
||||
WorkflowJobStatus,
|
||||
WorkflowJobType,
|
||||
WorkflowRunByIdWithJobsQuery,
|
||||
WorkflowRunFragment,
|
||||
WorkflowRunStatus,
|
||||
WorkflowRunTriggerEventType,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { Choice } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
export function computeTriggerInfoForWorkflowRun(run: WorkflowRunFragment): {
|
||||
triggerType: WorkflowTriggerType;
|
||||
@@ -68,16 +76,23 @@ export function choiceFromWorkflowJob(job: WorkflowJobResult, index: number): Ch
|
||||
};
|
||||
}
|
||||
|
||||
export function choicesFromWorkflowLogs(logs: WorkflowLogs): Choice[] {
|
||||
return Array.from(logs.keys()).map(step => {
|
||||
const logLines = logs.get(step);
|
||||
const stepStatus =
|
||||
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
|
||||
return {
|
||||
title: `${step} - ${stepStatus}`,
|
||||
value: step,
|
||||
};
|
||||
});
|
||||
export function choicesFromWorkflowLogs(
|
||||
logs: WorkflowLogs
|
||||
): (Choice & { name: string; status: string; logLines: WorkflowLogLine[] | undefined })[] {
|
||||
return Array.from(logs.keys())
|
||||
.map(step => {
|
||||
const logLines = logs.get(step);
|
||||
const stepStatus =
|
||||
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
|
||||
return {
|
||||
title: `${step} - ${stepStatus}`,
|
||||
name: step,
|
||||
status: stepStatus,
|
||||
value: step,
|
||||
logLines,
|
||||
};
|
||||
})
|
||||
.filter(step => step.status !== 'skipped');
|
||||
}
|
||||
|
||||
export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunResult[] {
|
||||
@@ -100,29 +115,171 @@ export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunRes
|
||||
});
|
||||
}
|
||||
|
||||
export async function processLogsFromJobAsync(
|
||||
export async function fetchAndProcessLogsFromJobAsync(
|
||||
state: { graphqlClient: ExpoGraphqlClient },
|
||||
job: WorkflowJobResult
|
||||
): Promise<WorkflowLogs | null> {
|
||||
const rawLogs = await fetchRawLogsForJobAsync(job);
|
||||
let rawLogs: string | null;
|
||||
switch (job.type) {
|
||||
case WorkflowJobType.Build:
|
||||
case WorkflowJobType.Repack:
|
||||
rawLogs = await fetchRawLogsForBuildJobAsync(state, job);
|
||||
break;
|
||||
default:
|
||||
rawLogs = await fetchRawLogsForCustomJobAsync(job);
|
||||
break;
|
||||
}
|
||||
if (!rawLogs) {
|
||||
return null;
|
||||
}
|
||||
Log.debug(`rawLogs = ${JSON.stringify(rawLogs, null, 2)}`);
|
||||
const logs: WorkflowLogs = new Map();
|
||||
const logKeys = new Set<string>();
|
||||
rawLogs.split('\n').forEach(line => {
|
||||
rawLogs.split('\n').forEach((line, index) => {
|
||||
Log.debug(`line ${index} = ${JSON.stringify(line, null, 2)}`);
|
||||
try {
|
||||
const parsedLine = JSON.parse(line);
|
||||
const { result, marker } = parsedLine;
|
||||
const { buildStepDisplayName, buildStepInternalId, time, msg } = parsedLine;
|
||||
const { buildStepDisplayName, buildStepInternalId, time, msg, result, marker, err } =
|
||||
parsedLine;
|
||||
const stepId = buildStepDisplayName ?? buildStepInternalId;
|
||||
if (stepId) {
|
||||
if (!logKeys.has(stepId)) {
|
||||
logKeys.add(stepId);
|
||||
logs.set(stepId, []);
|
||||
}
|
||||
logs.get(stepId)?.push({ time, msg, result, marker });
|
||||
logs.get(stepId)?.push({ time, msg, result, marker, err });
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
return logs;
|
||||
}
|
||||
|
||||
function descriptionForJobStatus(status: WorkflowJobStatus): string {
|
||||
switch (status) {
|
||||
case WorkflowJobStatus.New:
|
||||
return 'Waiting for worker';
|
||||
case WorkflowJobStatus.InProgress:
|
||||
return 'In progress';
|
||||
case WorkflowJobStatus.Success:
|
||||
return 'Completed successfully';
|
||||
case WorkflowJobStatus.Failure:
|
||||
return 'Failed';
|
||||
case WorkflowJobStatus.Canceled:
|
||||
return 'Canceled';
|
||||
case WorkflowJobStatus.Skipped:
|
||||
return 'Skipped';
|
||||
case WorkflowJobStatus.ActionRequired:
|
||||
return 'Waiting for action';
|
||||
case WorkflowJobStatus.PendingCancel:
|
||||
return 'Pending cancel';
|
||||
}
|
||||
}
|
||||
|
||||
export async function infoForActiveWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'],
|
||||
maxLogLines: number = 5 // -1 means no limit
|
||||
): Promise<string> {
|
||||
const statusLines = [];
|
||||
const statusValues = [];
|
||||
for (const job of workflowRun.jobs) {
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusValues.push({ label: ' Job', value: job.name });
|
||||
statusValues.push({ label: ' Status', value: descriptionForJobStatus(job.status) });
|
||||
if (job.status !== WorkflowJobStatus.InProgress) {
|
||||
continue;
|
||||
}
|
||||
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
|
||||
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
|
||||
if (steps.length > 0) {
|
||||
const currentStep = steps[steps.length - 1];
|
||||
statusValues.push({ label: ' Current step', value: currentStep.name });
|
||||
if (currentStep?.logLines?.length) {
|
||||
statusValues.push({ label: ' Current logs', value: '' });
|
||||
const currentLogs =
|
||||
currentStep.logLines
|
||||
?.map(line => line.msg)
|
||||
.filter((_, index) => {
|
||||
if (maxLogLines === -1) {
|
||||
return true;
|
||||
}
|
||||
return index > (currentStep.logLines?.length ?? 0) - maxLogLines;
|
||||
}) ?? [];
|
||||
for (const log of currentLogs) {
|
||||
statusValues.push({ label: '', value: log });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusLines.push(formatFields(statusValues));
|
||||
return statusLines.join('\n');
|
||||
}
|
||||
|
||||
export async function infoForFailedWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']
|
||||
): Promise<string> {
|
||||
const statusLines = [];
|
||||
const statusValues = [];
|
||||
for (const job of workflowRun.jobs) {
|
||||
if (job.status !== WorkflowJobStatus.Failure) {
|
||||
continue;
|
||||
}
|
||||
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
|
||||
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusValues.push({ label: ' Failed job', value: job.name });
|
||||
if (steps.length > 0) {
|
||||
const failedStep = steps.find(step => step.status === 'fail');
|
||||
if (failedStep) {
|
||||
const logs = failedStep.logLines?.map(line => line.msg) ?? [];
|
||||
statusValues.push({ label: ' Failed step', value: failedStep.name });
|
||||
statusValues.push({
|
||||
label: ' Logs for failed step',
|
||||
value: '',
|
||||
});
|
||||
for (const log of logs) {
|
||||
statusValues.push({ label: '', value: log });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusLines.push(formatFields(statusValues));
|
||||
return statusLines.join('\n');
|
||||
}
|
||||
export async function fileExistsAsync(filePath: string): Promise<boolean> {
|
||||
return await fs.promises
|
||||
.access(filePath, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
export async function maybeReadStdinAsync(): Promise<string | null> {
|
||||
// Check if there's data on stdin
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const trimmedData = data.trim();
|
||||
resolve(trimmedData || null);
|
||||
});
|
||||
|
||||
process.stdin.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { InvalidEasJsonError, MissingEasJsonError } from '@expo/eas-json/build/errors';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
import { buildProfileNamesFromProjectAsync } from './buildProfileUtils';
|
||||
import { getExpoApiWorkflowSchemaURL } from '../../api';
|
||||
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
|
||||
import Log from '../../log';
|
||||
import { createValidator, getReadableErrors } from '../../metadata/utils/ajv';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
const jobTypesWithBuildProfile = new Set(['build', 'repack']);
|
||||
|
||||
const buildProfileIsInterpolated = (profileName: string): boolean => {
|
||||
return profileName.includes('${{') && profileName.includes('}}');
|
||||
};
|
||||
|
||||
export async function validateWorkflowFileAsync(
|
||||
workflowFileContents: { yamlConfig: string; filePath: string },
|
||||
projectDir: string,
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
const parsedYaml = parsedYamlFromWorkflowContents(workflowFileContents);
|
||||
Log.debug(`Parsed workflow: ${JSON.stringify(parsedYaml, null, 2)}`);
|
||||
|
||||
// Check if the parsed result is empty or null
|
||||
Log.debug(`Validating workflow is not empty...`);
|
||||
validateWorkflowIsNotEmpty(parsedYaml);
|
||||
|
||||
const workflowSchema = await fetchWorkflowSchemaAsync();
|
||||
|
||||
// Check that all job types are valid
|
||||
Log.debug(`Validating workflow job types...`);
|
||||
validateWorkflowJobTypes(parsedYaml, workflowSchema);
|
||||
|
||||
// Check for build jobs that do not match any EAS build profiles
|
||||
Log.debug(`Validating workflow build jobs...`);
|
||||
await validateWorkflowBuildJobsAsync(parsedYaml, projectDir);
|
||||
|
||||
// Check that result passes validation against workflow schema
|
||||
Log.debug(`Validating workflow structure...`);
|
||||
validateWorkflowStructure(parsedYaml, workflowSchema);
|
||||
|
||||
// Check for other errors using the server-side validation
|
||||
Log.debug(`Validating workflow on server...`);
|
||||
await validateWorkflowOnServerAsync(graphqlClient, projectId, workflowFileContents);
|
||||
}
|
||||
|
||||
export function logWorkflowValidationErrors(error: unknown): void {
|
||||
if (error instanceof MissingEasJsonError) {
|
||||
throw new Error(
|
||||
'Workflows require a valid eas.json. Please run "eas build:configure" to create it.'
|
||||
);
|
||||
} else if (error instanceof InvalidEasJsonError) {
|
||||
throw new Error(
|
||||
'Workflows require a valid eas.json. Please fix the errors in your eas.json and try again.\n\n' +
|
||||
error.message
|
||||
);
|
||||
} else if (error instanceof YAML.YAMLParseError) {
|
||||
Log.error(`YAML syntax error: ${error.message}`);
|
||||
} else if (error instanceof CombinedError) {
|
||||
WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
||||
error,
|
||||
});
|
||||
|
||||
throw error;
|
||||
} else if (error instanceof Error) {
|
||||
Log.error(`Error: ${error.message}`);
|
||||
} else {
|
||||
Log.error(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateWorkflowIsNotEmpty(parsedYaml: any): void {
|
||||
if (
|
||||
parsedYaml === null ||
|
||||
parsedYaml === undefined ||
|
||||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
|
||||
) {
|
||||
throw new Error('YAML file is empty or contains only comments.');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateWorkflowOnServerAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
workflowFileContents: { yamlConfig: string }
|
||||
): Promise<void> {
|
||||
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
yamlConfig: workflowFileContents.yamlConfig,
|
||||
});
|
||||
}
|
||||
|
||||
async function validateWorkflowBuildJobsAsync(parsedYaml: any, projectDir: string): Promise<void> {
|
||||
const jobs = jobsFromWorkflow(parsedYaml);
|
||||
const buildJobs = jobs.filter(job => jobTypesWithBuildProfile.has(job.value.type));
|
||||
if (buildJobs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const buildProfileNames = await buildProfileNamesFromProjectAsync(projectDir);
|
||||
|
||||
const invalidBuildJobs = buildJobs.filter(
|
||||
job =>
|
||||
!buildProfileNames.has(job.value.params.profile) &&
|
||||
// If a profile name is interpolated, we can't check if it's valid until the workflow actually runs
|
||||
!buildProfileIsInterpolated(job.value.params.profile)
|
||||
);
|
||||
|
||||
if (invalidBuildJobs.length > 0) {
|
||||
const invalidBuildProfiles = new Set(invalidBuildJobs.map(job => job.value.params.profile));
|
||||
throw new Error(
|
||||
`The build jobs in this workflow refer to the following build profiles that are not present in your eas.json file: ${[
|
||||
...invalidBuildProfiles,
|
||||
].join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateWorkflowJobTypes(parsedYaml: any, workflowJsonSchema: any): void {
|
||||
const jobs = jobsFromWorkflow(parsedYaml);
|
||||
const jobTypes = jobTypesFromWorkflowSchema(workflowJsonSchema);
|
||||
const invalidJobs = jobs.filter(job => job.value.type && !jobTypes.includes(job.value.type));
|
||||
if (invalidJobs.length > 0) {
|
||||
throw new Error(
|
||||
`The following jobs have invalid types: ${invalidJobs
|
||||
.map(job => job.key)
|
||||
.join(', ')}. Valid types are: ${jobTypes.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateWorkflowStructure(parsedYaml: any, workflowJsonSchema: any): void {
|
||||
delete workflowJsonSchema['$schema'];
|
||||
|
||||
const ajv = createValidator();
|
||||
const validate = ajv.compile(workflowJsonSchema);
|
||||
const result = validate(parsedYaml);
|
||||
|
||||
if (!result) {
|
||||
Log.debug(
|
||||
JSON.stringify(
|
||||
{
|
||||
errors: validate.errors,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
const readableErrors = getReadableErrors(validate.errors ?? []);
|
||||
const processedErrors = new Set<string>();
|
||||
for (const err of readableErrors) {
|
||||
if (err.message) {
|
||||
processedErrors.add(err.message);
|
||||
}
|
||||
}
|
||||
throw new Error([...processedErrors].join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export function parsedYamlFromWorkflowContents(workflowFileContents: { yamlConfig: string }): any {
|
||||
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
|
||||
return parsedYaml;
|
||||
}
|
||||
|
||||
export function workflowContentsFromParsedYaml(parsedYaml: any): string {
|
||||
return YAML.stringify(parsedYaml);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
async function fetchWorkflowSchemaAsync(): Promise<any> {
|
||||
// EXPO_TESTING_WORKFLOW_SCHEMA_PATH is used only for testing against a different schema
|
||||
if (process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH) {
|
||||
const schemaPath = path.resolve(process.env.EXPO_TESTING_WORKFLOW_SCHEMA_PATH);
|
||||
Log.debug(`Loading workflow schema from ${schemaPath}`);
|
||||
const jsonString = await fs.readFile(schemaPath, 'utf-8');
|
||||
const jsonFromFile = JSON.parse(jsonString);
|
||||
return jsonFromFile.data;
|
||||
}
|
||||
// Otherwise, we fetch from <ApiBaseUrl>/v2/workflows/schema
|
||||
const schemaUrl = getExpoApiWorkflowSchemaURL();
|
||||
Log.debug(`Fetching workflow schema from ${schemaUrl}`);
|
||||
const response = await fetch(schemaUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to fetch EAS Workflow schema, received status: ${response.status}`);
|
||||
}
|
||||
const jsonResponse: any = (await response.json()) as any;
|
||||
return jsonResponse.data;
|
||||
}
|
||||
|
||||
function jobsFromWorkflow(parsedYaml: any): any[] {
|
||||
return Object.entries(parsedYaml?.jobs).flatMap(([key, value]: [string, any]) => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function jobTypesFromWorkflowSchema(workflowJsonSchema: any): string[] {
|
||||
return workflowJsonSchema?.properties?.jobs?.additionalProperties?.anyOf.map(
|
||||
(props: any) => props.properties.type.const
|
||||
);
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import Log, { learnMore } from '../../log';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { isExpoUpdatesInstalled, isUsingEASUpdate } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { ensureEASUpdateIsConfiguredInEasJsonAsync } from '../../update/configure';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
import { promptPlatformAsync } from '../../utils/prompts';
|
||||
|
||||
export default class BuildConfigure extends EasCommand {
|
||||
static override description = 'configure the project to support EAS Build';
|
||||
@@ -113,24 +113,7 @@ export default class BuildConfigure extends EasCommand {
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
Log.addNewLineIfNone();
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
return await promptPlatformAsync({
|
||||
message: 'Which platforms would you like to configure for EAS Build?',
|
||||
name: 'platform',
|
||||
choices: [
|
||||
{
|
||||
title: 'All',
|
||||
value: RequestedPlatform.All,
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
value: RequestedPlatform.Ios,
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
value: RequestedPlatform.Android,
|
||||
},
|
||||
],
|
||||
});
|
||||
return platform;
|
||||
}
|
||||
@@ -14,11 +14,13 @@ import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import Log, { link } from '../../log';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import uniq from '../../utils/expodash/uniq';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { maybeWarnAboutUsageOveragesAsync } from '../../utils/usage/checkForOverages';
|
||||
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
@@ -161,6 +163,13 @@ export default class Build extends EasCommand {
|
||||
? [StatuspageServiceName.EasBuild, StatuspageServiceName.EasSubmit]
|
||||
: [StatuspageServiceName.EasBuild]
|
||||
);
|
||||
|
||||
const { projectId } = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
BuildStatus,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
EnvironmentVariableEnvironment,
|
||||
IosJobOverridesInput,
|
||||
ProjectArchiveSourceType,
|
||||
StatuspageServiceName,
|
||||
@@ -48,7 +47,7 @@ interface BuildResignFlags {
|
||||
sourceProfile?: string;
|
||||
maybeBuildId?: string;
|
||||
wait: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
}
|
||||
interface RawBuildResignFlags {
|
||||
json: boolean;
|
||||
@@ -60,7 +59,7 @@ interface RawBuildResignFlags {
|
||||
'source-profile': string | undefined;
|
||||
wait: boolean;
|
||||
id: string | undefined;
|
||||
environment: EnvironmentVariableEnvironment | undefined;
|
||||
environment: string | undefined;
|
||||
}
|
||||
|
||||
export default class BuildResign extends EasCommand {
|
||||
|
||||
@@ -44,8 +44,6 @@ export default class WorkerAliasDelete extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -73,8 +73,6 @@ export default class WorkerAlias extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -45,8 +45,6 @@ export default class WorkerDelete extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as path from 'node:path';
|
||||
import { getHostingDeploymentsUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
@@ -45,7 +44,7 @@ interface DeployFlags {
|
||||
json: boolean;
|
||||
isProduction: boolean;
|
||||
aliasName?: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
deploymentIdentifier?: string;
|
||||
exportDir: string;
|
||||
dryRun: boolean;
|
||||
@@ -53,7 +52,7 @@ interface DeployFlags {
|
||||
|
||||
interface RawDeployFlags {
|
||||
'non-interactive': boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
json: boolean;
|
||||
prod: boolean;
|
||||
alias?: string;
|
||||
@@ -121,8 +120,6 @@ export default class WorkerDeploy extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -138,6 +135,7 @@ export default class WorkerDeploy extends EasCommand {
|
||||
logExportedProjectInfo(projectDist);
|
||||
|
||||
async function* emitWorkerTarballAsync(params: {
|
||||
routesConfig: WorkerAssets.RoutesConfigEntry | null;
|
||||
assetMap: WorkerAssets.AssetMap;
|
||||
manifest: WorkerAssets.Manifest;
|
||||
}): AsyncGenerator<WorkerAssets.FileEntry> {
|
||||
@@ -148,6 +146,8 @@ export default class WorkerDeploy extends EasCommand {
|
||||
for await (const workerFile of workerFiles) {
|
||||
yield [`server/${workerFile.normalizedPath}`, workerFile.data];
|
||||
}
|
||||
} else if (projectDist.type === 'static' && params.routesConfig) {
|
||||
yield ['routes.json', JSON.stringify(params.routesConfig)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,12 +284,13 @@ export default class WorkerDeploy extends EasCommand {
|
||||
manifestResult.conflictingVariableNames.join(' ')
|
||||
);
|
||||
}
|
||||
assetFiles = await WorkerAssets.collectAssetsAsync(
|
||||
projectDist.type === 'server' ? projectDist.clientPath : projectDist.path,
|
||||
{ maxFileSize: MAX_UPLOAD_SIZE }
|
||||
);
|
||||
const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
|
||||
assetFiles = await WorkerAssets.collectAssetsAsync(assetPath, {
|
||||
maxFileSize: MAX_UPLOAD_SIZE,
|
||||
});
|
||||
tarPath = await WorkerAssets.packFilesIterableAsync(
|
||||
emitWorkerTarballAsync({
|
||||
routesConfig: await WorkerAssets.getRoutesConfigAsync(assetPath),
|
||||
assetMap: WorkerAssets.assetsToAssetsMap(assetFiles),
|
||||
manifest: manifestResult.manifest,
|
||||
})
|
||||
|
||||
+51
-16
@@ -1,11 +1,11 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
@@ -47,6 +47,7 @@ describe(EnvCreate, () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
type: EnvironmentSecretType.String,
|
||||
}));
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.createSharedVariableAsync)
|
||||
@@ -56,6 +57,7 @@ describe(EnvCreate, () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
type: EnvironmentSecretType.String,
|
||||
}));
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.updateAsync)
|
||||
@@ -101,7 +103,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -138,7 +140,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
},
|
||||
]);
|
||||
@@ -166,7 +168,7 @@ describe(EnvCreate, () => {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
});
|
||||
});
|
||||
@@ -199,8 +201,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
isGlobal: true,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -235,7 +236,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: 'otherId',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
@@ -273,7 +274,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
@@ -284,7 +285,7 @@ describe(EnvCreate, () => {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
});
|
||||
@@ -319,12 +320,8 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
],
|
||||
environments: [DefaultEnvironment.Production, DefaultEnvironment.Development],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
isGlobal: true,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testAccountId
|
||||
@@ -343,7 +340,7 @@ describe(EnvCreate, () => {
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
|
||||
.mockImplementation(async () => [DefaultEnvironment.Production]);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -361,7 +358,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -369,4 +366,42 @@ describe(EnvCreate, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
const command = new EnvCreate(
|
||||
[
|
||||
'development',
|
||||
'--name',
|
||||
'TEST_VAR',
|
||||
'--value',
|
||||
'test-value',
|
||||
'--visibility',
|
||||
'plaintext',
|
||||
'--scope',
|
||||
'project',
|
||||
'--non-interactive',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariableMutation.createForAppAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
name: 'TEST_VAR',
|
||||
value: 'test-value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
@@ -35,7 +37,7 @@ describe(EnvDelete, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -121,4 +123,40 @@ describe(EnvDelete, () => {
|
||||
|
||||
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
const mockVariable = {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
|
||||
jest.mocked(EnvironmentVariableMutation.deleteAsync).mockResolvedValueOnce({ id: 'var1' });
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
const command = new EnvDelete(
|
||||
['development', '--variable-name', 'TEST_VAR_1', '--scope', 'project', '--non-interactive'],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import EnvExec from '../exec';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvExec, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
|
||||
const mockEnvironmentVariables = [
|
||||
{
|
||||
id: 'var1',
|
||||
name: 'EXPO_PUBLIC_API_URL',
|
||||
value: 'https://api.example.com',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
// Mock GraphQL query
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValue(mockEnvironmentVariables);
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
const command = new EnvExec(['development', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts custom environment when using positional argument', async () => {
|
||||
const command = new EnvExec(['custom-environment', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
+34
-4
@@ -1,10 +1,10 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
@@ -27,7 +27,7 @@ describe(EnvGet, () => {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -82,7 +82,7 @@ describe(EnvGet, () => {
|
||||
it('prompts for variable name and environment if the name is ambigous', async () => {
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
|
||||
.mockResolvedValueOnce(DefaultEnvironment.Production);
|
||||
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
|
||||
|
||||
const command = new EnvGet([], mockConfig);
|
||||
@@ -98,7 +98,7 @@ describe(EnvGet, () => {
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -111,4 +111,34 @@ describe(EnvGet, () => {
|
||||
expect(promptVariableEnvironmentAsync).toHaveBeenCalled();
|
||||
expect(promptVariableNameAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValueOnce(mockVariables);
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
const command = new EnvGet(
|
||||
['development', '--variable-name', 'TEST_VAR_1', '--scope', 'project'],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
+42
-28
@@ -1,11 +1,11 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
@@ -13,10 +13,12 @@ import {
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptVariableEnvironmentAsync } from '../../../utils/prompts';
|
||||
import EnvList from '../list';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
jest.mock('../../../log');
|
||||
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
@@ -24,7 +26,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
@@ -35,7 +37,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_2',
|
||||
value: 'value2',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
@@ -51,27 +53,19 @@ describe(EnvList, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('lists project environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
|
||||
async (args: any) => {
|
||||
if (args.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
}
|
||||
if (args.multiple) {
|
||||
return [DefaultEnvironment.Production];
|
||||
}
|
||||
return DefaultEnvironment.Production;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('lists project environment variables in specified environments', async () => {
|
||||
@@ -88,13 +82,33 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Production,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList(['development'], mockConfig); // Using positional argument
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
includeFileContent: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('lists project environment variables including sensitive values', async () => {
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
@@ -113,7 +127,7 @@ describe(EnvList, () => {
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
}
|
||||
);
|
||||
@@ -135,7 +149,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
@@ -158,7 +172,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import {
|
||||
EnvironmentVariableWithFileContent,
|
||||
EnvironmentVariablesQuery,
|
||||
} from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import EnvPull from '../pull';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../log');
|
||||
|
||||
describe(EnvPull, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const testProjectDir = '/test/project';
|
||||
const testTargetPath = '.env.local';
|
||||
|
||||
const mockEnvironmentVariables: EnvironmentVariableWithFileContent[] = [
|
||||
{
|
||||
id: 'var1',
|
||||
name: 'EXPO_PUBLIC_API_URL',
|
||||
value: 'https://api.example.com',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
{
|
||||
id: 'var2',
|
||||
name: 'DATABASE_URL',
|
||||
value: 'postgres://localhost:5432/mydb',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
{
|
||||
id: 'var3',
|
||||
name: 'SECRET_KEY',
|
||||
value: 'super-secret-key',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
{
|
||||
id: 'var4',
|
||||
name: 'CONFIG_FILE',
|
||||
value: 'base64-encoded-file-content',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
valueWithFileContent: Buffer.from('{"key": "value"}').toString('base64'),
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(Log, 'addNewLineIfNone').mockImplementation(() => {});
|
||||
|
||||
// Mock fs-extra methods
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(false));
|
||||
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.mkdir).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
|
||||
|
||||
// Mock GraphQL query
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
|
||||
.mockResolvedValue(mockEnvironmentVariables);
|
||||
});
|
||||
|
||||
describe('environment validation', () => {
|
||||
it('accepts development environment', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'development',
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts custom environment', async () => {
|
||||
const command = new EnvPull(['custom-environment'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file operations', () => {
|
||||
it('writes environment variables to .env.local file', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
testTargetPath,
|
||||
expect.stringContaining('# Environment: development')
|
||||
);
|
||||
});
|
||||
|
||||
it('writes correct environment variable content', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Find the call that writes to .env.local (not the file variable)
|
||||
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
|
||||
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
|
||||
expect(envFileCall).toBeDefined();
|
||||
const fileContent = envFileCall![1] as string;
|
||||
|
||||
// Check that the file contains the expected content
|
||||
expect(fileContent).toContain('# Environment: development');
|
||||
expect(fileContent).toContain('EXPO_PUBLIC_API_URL=https://api.example.com');
|
||||
expect(fileContent).toContain('DATABASE_URL=postgres://localhost:5432/mydb');
|
||||
expect(fileContent).toContain('# SECRET_KEY=***** (secret)');
|
||||
});
|
||||
|
||||
it('handles file variables correctly', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should create .eas/.env directory for file variables
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(path.join(testProjectDir, '.eas', '.env'), {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Should write the file content (base64 encoded)
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
path.join(testProjectDir, '.eas', '.env', 'CONFIG_FILE'),
|
||||
Buffer.from('{"key": "value"}').toString('base64'),
|
||||
'base64'
|
||||
);
|
||||
|
||||
// Should include file path in env file
|
||||
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
|
||||
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
|
||||
expect(envFileCall).toBeDefined();
|
||||
const fileContent = envFileCall![1] as string;
|
||||
expect(fileContent).toContain('CONFIG_FILE=');
|
||||
expect(fileContent).toContain(path.join(testProjectDir, '.eas', '.env', 'CONFIG_FILE'));
|
||||
});
|
||||
|
||||
it('prompts for overwrite when file exists in interactive mode', async () => {
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message: `File ${testTargetPath} already exists. Do you want to overwrite it?`,
|
||||
});
|
||||
});
|
||||
|
||||
it('aborts when user declines overwrite', async () => {
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
jest.mocked(confirmAsync).mockResolvedValue(false);
|
||||
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(`File ${testTargetPath} already exists.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
it('works in non-interactive mode', async () => {
|
||||
const command = new EnvPull(['development', '--non-interactive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'development',
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('requires environment in non-interactive mode', async () => {
|
||||
const command = new EnvPull(['--non-interactive'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom target path', () => {
|
||||
it('writes to custom path when specified', async () => {
|
||||
const customPath = '.env.custom';
|
||||
const command = new EnvPull(['development', '--path', customPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
customPath,
|
||||
expect.stringContaining('# Environment: development')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging', () => {
|
||||
it('logs success message with correct environment', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
`Pulled plain text and sensitive environment variables from "development" environment to ${testTargetPath}.`
|
||||
);
|
||||
});
|
||||
|
||||
it('logs secret variables that were skipped', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('The following variables have the secret visibility')
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('SECRET_KEY'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('existing .env.local file handling', () => {
|
||||
it('preserves existing secret values when they exist', async () => {
|
||||
const existingEnvContent = 'SECRET_KEY=existing-secret-value\nOTHER_VAR=other-value';
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(existingEnvContent));
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Find the call that writes to .env.local (not the file variable)
|
||||
const writeFileCalls = jest.mocked(fs.writeFile).mock.calls;
|
||||
const envFileCall = writeFileCalls.find(call => call[0] === testTargetPath);
|
||||
expect(envFileCall).toBeDefined();
|
||||
const fileContent = envFileCall![1] as string;
|
||||
|
||||
// Should use existing secret value instead of placeholder
|
||||
expect(fileContent).toContain('SECRET_KEY=existing-secret-value');
|
||||
expect(fileContent).not.toContain('# SECRET_KEY=***** (secret)');
|
||||
|
||||
// Should log that it reused the local value
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Reused local values for following secrets: SECRET_KEY')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import EnvPush from '../push';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
describe(EnvPush, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = {} as unknown as Config;
|
||||
const testProjectDir = '/test/project';
|
||||
const testEnvPath = '.env.test';
|
||||
|
||||
const mockEnvContent = `EXPO_PUBLIC_API_URL=https://api.example.com
|
||||
VARIABLE_NAME=variable value
|
||||
SECRET_KEY=super-secret-key`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(Log, 'warn').mockImplementation(() => {});
|
||||
jest.spyOn(Log, 'error').mockImplementation(() => {});
|
||||
|
||||
// Mock fs-extra methods
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(mockEnvContent));
|
||||
|
||||
// Mock GraphQL queries and mutations
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([]); // No existing variables
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync)
|
||||
.mockResolvedValue(true);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts custom environment', async () => {
|
||||
const command = new EnvPush(['custom-environment', '--path', testEnvPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('--force option', () => {
|
||||
it('automatically overrides existing variables without confirmation when --force is used', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should not call confirmAsync for existing variables
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
|
||||
// Should log that force flag is being used
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
|
||||
// Should call the mutation with overwrite flag
|
||||
expect(
|
||||
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
|
||||
).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
overwrite: true,
|
||||
}),
|
||||
]),
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
|
||||
it('automatically overrides sensitive variables without confirmation when --force is used', async () => {
|
||||
const existingSensitiveVariable = {
|
||||
id: 'var1',
|
||||
name: 'SECRET_KEY',
|
||||
value: 'old-secret-key',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
|
||||
.mockResolvedValue([existingSensitiveVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should not call confirmAsync for sensitive variables
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
|
||||
// Should log that force flag is being used for sensitive variables
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
|
||||
// Should call the mutation with overwrite flag
|
||||
expect(
|
||||
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
|
||||
).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'SECRET_KEY',
|
||||
value: 'super-secret-key',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
overwrite: true,
|
||||
}),
|
||||
]),
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
|
||||
it('logs appropriate messages when using --force flag', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should log both force messages
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
});
|
||||
|
||||
it('still prompts for confirmation when --force is not used', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should call confirmAsync for both existing variables and sensitive variables
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(2);
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message:
|
||||
'The VARIABLE_NAME environment variable already exists in development environment. Do you want to override it?',
|
||||
});
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message:
|
||||
'You are about to overwrite sensitive variables.\n- VARIABLE_NAME\n Do you want to continue?',
|
||||
});
|
||||
|
||||
// Should not log force messages
|
||||
expect(Log.log).not.toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
expect(Log.log).not.toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,8 +2,9 @@ import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../../graphql/generated';
|
||||
@@ -11,12 +12,18 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import {
|
||||
parseVisibility,
|
||||
promptVariableEnvironmentAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../../utils/prompts';
|
||||
import EnvUpdate from '../update';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../utils/prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
@@ -33,6 +40,41 @@ describe(EnvUpdate, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
|
||||
async (args: any) => {
|
||||
if (args.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
}
|
||||
if (args.multiple) {
|
||||
return ['production'];
|
||||
}
|
||||
return 'production';
|
||||
}
|
||||
);
|
||||
(jest.mocked(promptVariableVisibilityAsync) as jest.MockedFunction<any>).mockResolvedValue(
|
||||
EnvironmentVariableVisibility.Public
|
||||
);
|
||||
(jest.mocked(parseVisibility) as jest.MockedFunction<any>).mockImplementation(
|
||||
(visibility: string) => {
|
||||
switch (visibility) {
|
||||
case 'plaintext':
|
||||
return EnvironmentVariableVisibility.Public;
|
||||
case 'sensitive':
|
||||
return EnvironmentVariableVisibility.Sensitive;
|
||||
case 'secret':
|
||||
return EnvironmentVariableVisibility.Secret;
|
||||
default:
|
||||
throw new Error(`Invalid visibility: ${visibility}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
(jest.mocked(selectAsync) as jest.MockedFunction<any>).mockResolvedValue({
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a project variable by selected name in non-interactive mode', async () => {
|
||||
@@ -41,7 +83,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -73,7 +115,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
@@ -87,7 +129,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -119,7 +161,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
|
||||
@@ -129,6 +171,7 @@ describe(EnvUpdate, () => {
|
||||
it('prompts for variable selection when selected name is not provided', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
{ id: '2', name: 'ANOTHER_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
@@ -140,7 +183,7 @@ describe(EnvUpdate, () => {
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.objectContaining({ id: variableId })
|
||||
@@ -162,4 +205,50 @@ describe(EnvUpdate, () => {
|
||||
'Variable with name NON_EXISTENT_VARIABLE does not exist on project @testuser/testpp.'
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
const mockVariable = {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
|
||||
jest.mocked(EnvironmentVariableMutation.updateAsync).mockResolvedValueOnce(mockVariable);
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'development',
|
||||
'--variable-name',
|
||||
'TEST_VAR_1',
|
||||
'--value',
|
||||
'new-value',
|
||||
'--scope',
|
||||
'project',
|
||||
'--non-interactive',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
+22
-25
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../graphql/generated';
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
interface RawCreateFlags {
|
||||
name?: string;
|
||||
@@ -42,7 +41,7 @@ interface RawCreateFlags {
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ interface CreateFlags {
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ export default class EnvCreate extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to create the variable in. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to create the variable in. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -101,6 +100,13 @@ export default class EnvCreate extends EasCommand {
|
||||
|
||||
const validatedFlags = this.sanitizeFlags(flags);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive: validatedFlags['non-interactive'],
|
||||
});
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
@@ -111,14 +117,7 @@ export default class EnvCreate extends EasCommand {
|
||||
force,
|
||||
type,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(validatedFlags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
} = await this.promptForMissingFlagsAsync(validatedFlags, args, { graphqlClient, projectId });
|
||||
|
||||
const [projectDisplayName, ownerAccount] = await Promise.all([
|
||||
getDisplayNameForProjectIdAsync(graphqlClient, projectId),
|
||||
@@ -220,7 +219,6 @@ export default class EnvCreate extends EasCommand {
|
||||
value,
|
||||
visibility,
|
||||
environments,
|
||||
isGlobal: true, // TODO: every account-wide variable is global for now so it's not user facing
|
||||
type: type ?? EnvironmentSecretType.String,
|
||||
},
|
||||
ownerAccount.id
|
||||
@@ -273,7 +271,8 @@ export default class EnvCreate extends EasCommand {
|
||||
type,
|
||||
...rest
|
||||
}: CreateFlags,
|
||||
{ environment }: { environment?: string }
|
||||
{ environment }: { environment?: string },
|
||||
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
|
||||
): Promise<
|
||||
Required<
|
||||
Omit<CreateFlags, 'type' | 'visibility'> & {
|
||||
@@ -324,18 +323,16 @@ export default class EnvCreate extends EasCommand {
|
||||
|
||||
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
|
||||
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
let newEnvironments = environments
|
||||
? environments
|
||||
: environment
|
||||
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
|
||||
: undefined;
|
||||
let newEnvironments = environments ? environments : environment ? [environment] : undefined;
|
||||
|
||||
if (!newEnvironments) {
|
||||
newEnvironments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
newEnvironments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (!newEnvironments || newEnvironments.length === 0) {
|
||||
throw new Error('No environments selected');
|
||||
|
||||
+6
-13
@@ -9,23 +9,23 @@ import {
|
||||
EASNonInteractiveFlag,
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import { EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariableName } from '../../utils/variableUtils';
|
||||
|
||||
interface DeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
scope: EnvironmentVariableScope;
|
||||
}
|
||||
|
||||
interface RawDeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export default class EnvDelete extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable to delete',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to delete',
|
||||
}),
|
||||
@@ -49,7 +49,7 @@ export default class EnvDelete extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to delete. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable to delete. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -165,13 +165,6 @@ export default class EnvDelete extends EasCommand {
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return { ...flags, 'variable-environment': environment, scope };
|
||||
}
|
||||
|
||||
|
||||
+9
-13
@@ -4,21 +4,19 @@ import chalk from 'chalk';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type ParsedFlags =
|
||||
| {
|
||||
nonInteractive: true;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
command: string;
|
||||
}
|
||||
| {
|
||||
nonInteractive: false;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
@@ -44,7 +42,7 @@ export default class EnvExec extends EasCommand {
|
||||
name: 'environment',
|
||||
required: true,
|
||||
description:
|
||||
"Environment to execute the command in. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to execute the command in. Default environments are 'production', 'preview', and 'development'.",
|
||||
},
|
||||
{
|
||||
name: 'bash_command',
|
||||
@@ -70,7 +68,11 @@ export default class EnvExec extends EasCommand {
|
||||
|
||||
const environment =
|
||||
parsedFlags.environment ??
|
||||
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
|
||||
(await promptVariableEnvironmentAsync({
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
}));
|
||||
const environmentVariables = await this.loadEnvironmentVariablesAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
@@ -101,12 +103,6 @@ export default class EnvExec extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
environment = environment?.toUpperCase();
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const firstChar = bash_command[0];
|
||||
const lastChar = bash_command[bash_command.length - 1];
|
||||
const cleanCommand =
|
||||
@@ -198,7 +194,7 @@ export default class EnvExec extends EasCommand {
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<Record<string, string>> {
|
||||
const environmentVariablesQueryResult =
|
||||
|
||||
+8
-13
@@ -11,7 +11,6 @@ import {
|
||||
EasEnvironmentFlagParameters,
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
@@ -19,11 +18,11 @@ import {
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
|
||||
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariable, formatVariableValue } from '../../utils/variableUtils';
|
||||
|
||||
interface RawGetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
@@ -31,7 +30,7 @@ interface RawGetFlags {
|
||||
|
||||
interface GetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
@@ -49,7 +48,7 @@ export default class EnvGet extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -58,7 +57,7 @@ export default class EnvGet extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable',
|
||||
}),
|
||||
@@ -93,6 +92,8 @@ export default class EnvGet extends EasCommand {
|
||||
environment = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: false,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,12 +157,6 @@ export default class EnvGet extends EasCommand {
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
@@ -177,7 +172,7 @@ async function getVariablesAsync(
|
||||
scope: string,
|
||||
projectId: string,
|
||||
name: string | undefined,
|
||||
environment: EnvironmentVariableEnvironment | undefined
|
||||
environment: string | undefined
|
||||
): Promise<EnvironmentVariableFragment[]> {
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@ import {
|
||||
EASMultiEnvironmentFlag,
|
||||
EASVariableFormatFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import { EnvironmentVariableScope } from '../../graphql/generated';
|
||||
import {
|
||||
EnvironmentVariableWithFileContent,
|
||||
EnvironmentVariablesQuery,
|
||||
@@ -19,7 +19,6 @@ import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import {
|
||||
formatVariable,
|
||||
formatVariableValue,
|
||||
isEnvironment,
|
||||
performForEnvironmentsAsync,
|
||||
} from '../../utils/variableUtils';
|
||||
|
||||
@@ -35,7 +34,7 @@ async function getVariablesForScopeAsync(
|
||||
scope: EnvironmentVariableScope;
|
||||
includingSensitive: boolean;
|
||||
includeFileContent: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<EnvironmentVariableWithFileContent[]> {
|
||||
@@ -70,7 +69,7 @@ async function getVariablesForScopeAsync(
|
||||
interface RawListFlags {
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
environment: string[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive'?: boolean;
|
||||
@@ -79,7 +78,7 @@ interface RawListFlags {
|
||||
interface ListFlags {
|
||||
scope: EnvironmentVariableScope;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
environment: string[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive': boolean;
|
||||
@@ -111,7 +110,7 @@ export default class EnvList extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to list the variables from. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to list the variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -136,7 +135,12 @@ export default class EnvList extends EasCommand {
|
||||
});
|
||||
|
||||
if (!environments) {
|
||||
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
await performForEnvironmentsAsync(environments, async environment => {
|
||||
@@ -179,14 +183,10 @@ export default class EnvList extends EasCommand {
|
||||
flags: RawListFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): ListFlags {
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments = flags.environment
|
||||
? flags.environment
|
||||
: environment
|
||||
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
|
||||
? [environment]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
+10
-11
@@ -13,7 +13,6 @@ import {
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvPull extends EasCommand {
|
||||
static override description =
|
||||
@@ -29,7 +28,7 @@ export default class EnvPull extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to pull variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -49,15 +48,7 @@ export default class EnvPull extends EasCommand {
|
||||
flags: { environment: flagEnvironment, path: targetPath, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(EnvPull);
|
||||
|
||||
let environment = flagEnvironment?.toUpperCase() ?? argEnvironment?.toUpperCase();
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync({ nonInteractive });
|
||||
}
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
let environment = flagEnvironment?.toLowerCase() ?? argEnvironment?.toLowerCase();
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@@ -67,6 +58,14 @@ export default class EnvPull extends EasCommand {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
targetPath = targetPath ?? '.env.local';
|
||||
|
||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
|
||||
|
||||
+60
-52
@@ -6,20 +6,16 @@ import path from 'path';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
CreateEnvironmentVariableInput,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../graphql/generated';
|
||||
import {
|
||||
EnvironmentVariableMutation,
|
||||
EnvironmentVariablePushInput,
|
||||
} from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvPush extends EasCommand {
|
||||
static override description =
|
||||
@@ -36,13 +32,17 @@ export default class EnvPush extends EasCommand {
|
||||
description: 'Path to the input `.env` file',
|
||||
default: '.env.local',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Skip confirmation and automatically override existing variables',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to push variables to. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to push variables to. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -50,7 +50,7 @@ export default class EnvPush extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvPush);
|
||||
|
||||
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
|
||||
let { environment: environments, path: envPath, force } = this.parseFlagsAndArgs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@@ -63,11 +63,13 @@ export default class EnvPush extends EasCommand {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive: false,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
const updateVariables: Record<string, EnvironmentVariablePushInput> =
|
||||
await this.parseEnvFileAsync(envPath, environments);
|
||||
const updateVariables = await this.parseEnvFileAsync(envPath, environments);
|
||||
|
||||
const variableNames = Object.keys(updateVariables);
|
||||
|
||||
@@ -117,40 +119,46 @@ export default class EnvPush extends EasCommand {
|
||||
Log.warn(`Some variables already exist in the ${displayedEnvironment} environment.`);
|
||||
const variableNames = existingDifferentVariables.map(variable => variable.name);
|
||||
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
|
||||
let variablesToOverwrite: string[] = [];
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 0) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
if (force) {
|
||||
// When --force is used, automatically override all existing variables
|
||||
Log.log('Using --force flag: automatically overriding existing variables.');
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 0) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingVariable of existingVariables) {
|
||||
@@ -168,7 +176,7 @@ export default class EnvPush extends EasCommand {
|
||||
variable => variable.visibility !== EnvironmentVariableVisibility.Public
|
||||
);
|
||||
|
||||
if (existingSensitiveVariables.length > 0) {
|
||||
if (existingSensitiveVariables.length > 0 && !force) {
|
||||
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
|
||||
variable => `- ${variable.name}`
|
||||
);
|
||||
@@ -180,6 +188,8 @@ export default class EnvPush extends EasCommand {
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting...');
|
||||
}
|
||||
} else if (existingSensitiveVariables.length > 0 && force) {
|
||||
Log.log('Using --force flag: automatically overriding sensitive variables.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,16 +209,14 @@ export default class EnvPush extends EasCommand {
|
||||
}
|
||||
|
||||
parseFlagsAndArgs(
|
||||
flags: { path: string; environment: EnvironmentVariableEnvironment[] | undefined },
|
||||
flags: {
|
||||
path: string;
|
||||
environment: string[] | undefined;
|
||||
force: boolean;
|
||||
},
|
||||
{ environment }: Record<string, string>
|
||||
): { environment?: EnvironmentVariableEnvironment[]; path: string } {
|
||||
if (environment && !isEnvironment(environment.toUpperCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments =
|
||||
flags.environment ??
|
||||
(environment ? [environment.toUpperCase() as EnvironmentVariableEnvironment] : undefined);
|
||||
): { environment?: string[]; path: string; force: boolean } {
|
||||
const environments = flags.environment ?? (environment ? [environment] : undefined);
|
||||
|
||||
return {
|
||||
...flags,
|
||||
@@ -218,12 +226,12 @@ export default class EnvPush extends EasCommand {
|
||||
|
||||
private async parseEnvFileAsync(
|
||||
envPath: string,
|
||||
environments: EnvironmentVariableEnvironment[]
|
||||
): Promise<Record<string, EnvironmentVariablePushInput>> {
|
||||
environments: string[]
|
||||
): Promise<Record<string, CreateEnvironmentVariableInput & { environments: string[] }>> {
|
||||
if (!(await fs.exists(envPath))) {
|
||||
throw new Error(`File ${envPath} does not exist.`);
|
||||
}
|
||||
const pushInput: Record<string, EnvironmentVariablePushInput> = {};
|
||||
const pushInput: Awaited<ReturnType<typeof this.parseEnvFileAsync>> = {};
|
||||
|
||||
const variables: Record<string, string> = dotenv.parse(await fs.readFile(envPath, 'utf8'));
|
||||
|
||||
|
||||
+26
-24
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
} from '../../commandUtils/flags';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
@@ -36,17 +36,17 @@ import {
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariableName } from '../../utils/variableUtils';
|
||||
|
||||
interface RawUpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ interface UpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default class EnvUpdate extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Current name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to update',
|
||||
}),
|
||||
@@ -93,7 +93,7 @@ export default class EnvUpdate extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to update. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable to update. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -180,15 +180,19 @@ export default class EnvUpdate extends EasCommand {
|
||||
visibility: newVisibility,
|
||||
type: newType,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(selectedVariable, {
|
||||
name,
|
||||
value: rawValue,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
});
|
||||
} = await this.promptForMissingFlagsAsync(
|
||||
selectedVariable,
|
||||
{
|
||||
name,
|
||||
value: rawValue,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
},
|
||||
{ graphqlClient, projectId }
|
||||
);
|
||||
|
||||
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
id: selectedVariable.id,
|
||||
@@ -226,12 +230,6 @@ export default class EnvUpdate extends EasCommand {
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toUpperCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
@@ -252,7 +250,8 @@ export default class EnvUpdate extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
...rest
|
||||
}: UpdateFlags
|
||||
}: UpdateFlags,
|
||||
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
|
||||
): Promise<
|
||||
Omit<UpdateFlags, 'type' | 'visibility'> & {
|
||||
type?: EnvironmentSecretType;
|
||||
@@ -316,7 +315,10 @@ export default class EnvUpdate extends EasCommand {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
selectedEnvironments: selectedVariable.environments ?? [],
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
EnvironmentVariableEnvironment,
|
||||
FingerprintFragment,
|
||||
UpdateFragment,
|
||||
} from '../../graphql/generated';
|
||||
@@ -97,7 +96,7 @@ export default class FingerprintCompare extends EasCommand {
|
||||
open: Flags.boolean({
|
||||
description: 'Open the fingerprint comparison in the browser',
|
||||
}),
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description:
|
||||
'If generating a fingerprint from the local directory, use the specified environment.',
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getFingerprintInfoFromLocalProjectForPlatformsAsync,
|
||||
stringToAppPlatform,
|
||||
} from '../../fingerprint/utils';
|
||||
import { AppPlatform, EnvironmentVariableEnvironment } from '../../graphql/generated';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -39,7 +39,7 @@ export default class FingerprintGenerate extends EasCommand {
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
exclusive: ['build-profile'],
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import {
|
||||
createOrModifyExpoConfigAsync,
|
||||
getPrivateExpoConfigAsync,
|
||||
} from '../../../project/expoConfig';
|
||||
import New, { createProjectAsync, generateConfigsAsync } from '../new';
|
||||
|
||||
jest.mock('fs-extra', () => ({
|
||||
...jest.requireActual('fs-extra'),
|
||||
pathExists: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../../../prompts', () => ({
|
||||
promptAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../project/expoConfig');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
getActorUsername: jest.fn().mockReturnValue('jester'),
|
||||
}));
|
||||
jest.mock('../../../ora');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../onboarding/installDependencies');
|
||||
jest.mock('../../../api');
|
||||
jest.mock('../../../build/utils/url');
|
||||
jest.mock('../../../utils/easCli', () => ({
|
||||
easCliVersion: '5.0.0',
|
||||
}));
|
||||
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync', () => ({
|
||||
findProjectIdByAccountNameAndSlugNullableAsync: jest.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
jest.mock('../../../graphql/mutations/AppMutation', () => ({
|
||||
AppMutation: {
|
||||
createAppAsync: jest.fn().mockResolvedValue('test-project-id'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../project/expoConfig', () => ({
|
||||
getPrivateExpoConfigAsync: jest.fn().mockResolvedValue({ extra: {} }),
|
||||
createOrModifyExpoConfigAsync: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
jest.mock('../../../prompts', () => ({
|
||||
...jest.requireActual('../../../prompts'),
|
||||
promptAsync: jest.fn().mockImplementation(async (options: any) => {
|
||||
if (options.name === 'account') {
|
||||
return { account: { name: 'jester' } };
|
||||
}
|
||||
if (options.name === 'name') {
|
||||
return { name: options.initial || 'expo-project' };
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe(New.name, () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
describe('generateConfigsAsync', () => {
|
||||
it('should orchestrate account selection and project configuration', async () => {
|
||||
const result = await generateConfigsAsync({ path: undefined }, jester, mockGraphqlClient);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectAccount: 'jester',
|
||||
projectDirectory: expect.stringContaining('/expo-project'),
|
||||
projectName: 'expo-project',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass through path argument to config generation', async () => {
|
||||
const customPath = 'my-custom-project';
|
||||
|
||||
const result = await generateConfigsAsync({ path: customPath }, jester, mockGraphqlClient);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectAccount: 'jester',
|
||||
projectDirectory: expect.stringContaining('/my-custom-project'),
|
||||
projectName: 'my-custom-project',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProjectAsync', () => {
|
||||
it('should create a project', async () => {
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('test-project-id');
|
||||
jest
|
||||
.mocked(getPrivateExpoConfigAsync)
|
||||
.mockResolvedValue({ name: 'name', slug: 'slug' } as ExpoConfig);
|
||||
jest.mocked(createOrModifyExpoConfigAsync);
|
||||
|
||||
const result = await createProjectAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
actor: jester,
|
||||
projectDirectory: 'test-project',
|
||||
projectAccount: 'jester',
|
||||
projectName: 'test',
|
||||
});
|
||||
|
||||
expect(result).toBe('test-project-id');
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
|
||||
accountId: jester.accounts[0].id,
|
||||
projectName: 'test',
|
||||
});
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
'test-project',
|
||||
{
|
||||
extra: { eas: { projectId: 'test-project-id' } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
cloneTemplateAsync,
|
||||
initializeGitRepositoryAsync,
|
||||
installProjectDependenciesAsync,
|
||||
} from '../../commandUtils/new/commands';
|
||||
import {
|
||||
generateProjectConfigAsync,
|
||||
promptForProjectAccountAsync,
|
||||
} from '../../commandUtils/new/configs';
|
||||
import {
|
||||
copyProjectTemplatesAsync,
|
||||
generateAppConfigAsync,
|
||||
generateEasConfigAsync,
|
||||
updatePackageJsonAsync,
|
||||
updateReadmeAsync,
|
||||
} from '../../commandUtils/new/projectFiles';
|
||||
import { printDirectory } from '../../commandUtils/new/utils';
|
||||
import { AppFragment } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { PACKAGE_MANAGERS, PackageManager } from '../../onboarding/installDependencies';
|
||||
import { ora } from '../../ora';
|
||||
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
export async function generateConfigsAsync(
|
||||
args: { path?: string },
|
||||
actor: Actor,
|
||||
graphqlClient: ExpoGraphqlClient
|
||||
): Promise<{
|
||||
projectName: string;
|
||||
projectDirectory: string;
|
||||
projectAccount: string;
|
||||
}> {
|
||||
const projectAccount = await promptForProjectAccountAsync(actor);
|
||||
const { projectName, projectDirectory } = await generateProjectConfigAsync(args.path, {
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
});
|
||||
|
||||
return {
|
||||
projectAccount,
|
||||
projectDirectory,
|
||||
projectName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createProjectAsync({
|
||||
graphqlClient,
|
||||
actor,
|
||||
projectDirectory,
|
||||
projectAccount,
|
||||
projectName,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
actor: Actor;
|
||||
projectDirectory: string;
|
||||
projectAccount: string;
|
||||
projectName: string;
|
||||
}): Promise<string> {
|
||||
const projectFullName = `@${projectAccount}/${projectName}`;
|
||||
const projectDashboardUrl = getProjectDashboardUrl(projectAccount, projectName);
|
||||
const projectLink = link(projectDashboardUrl, { text: projectFullName });
|
||||
|
||||
const account = nullthrows(actor.accounts.find(a => a.name === projectAccount));
|
||||
|
||||
const spinner = ora(`Creating ${chalk.bold(projectFullName)}`).start();
|
||||
let projectId: string;
|
||||
try {
|
||||
projectId = await AppMutation.createAppAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
projectName,
|
||||
});
|
||||
spinner.succeed(`Created ${chalk.bold(projectLink)}`);
|
||||
} catch (err) {
|
||||
spinner.fail();
|
||||
throw err;
|
||||
}
|
||||
|
||||
const exp = await getPrivateExpoConfigAsync(projectDirectory, { skipPlugins: true });
|
||||
await createOrModifyExpoConfigAsync(
|
||||
projectDirectory,
|
||||
{
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
Log.withInfo(`Project successfully linked (ID: ${chalk.bold(projectId)})`);
|
||||
|
||||
return projectId;
|
||||
}
|
||||
|
||||
export async function generateProjectFilesAsync(
|
||||
projectDir: string,
|
||||
app: AppFragment,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const spinner = ora(`Generating project files`).start();
|
||||
await generateAppConfigAsync(projectDir, app);
|
||||
|
||||
await generateEasConfigAsync(projectDir);
|
||||
|
||||
await updatePackageJsonAsync(projectDir);
|
||||
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
await updateReadmeAsync(projectDir, packageManager);
|
||||
spinner.succeed(`Generated project files`);
|
||||
Log.withInfo(
|
||||
`Generated ${chalk.bold('app.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Generated ${chalk.bold('eas.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
export default class New extends EasCommand {
|
||||
static override aliases = ['new'];
|
||||
|
||||
static override description =
|
||||
'Create a new project configured with Expo Application Services (EAS)';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'path',
|
||||
description: 'Path to create the project (defaults to current directory)',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
'package-manager': Flags.enum<PackageManager>({
|
||||
char: 'p',
|
||||
description: 'Package manager to use for installing dependencies',
|
||||
options: [...PACKAGE_MANAGERS],
|
||||
default: 'npm',
|
||||
}),
|
||||
};
|
||||
|
||||
static override hidden = true;
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(New);
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
} = await this.getContextAsync(New, { nonInteractive: false });
|
||||
|
||||
if (actor.__typename === 'Robot') {
|
||||
throw new Error(
|
||||
'This command is not available for robot users. Make sure you are not using a robot token and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
|
||||
|
||||
const {
|
||||
projectName,
|
||||
projectDirectory: targetProjectDirectory,
|
||||
projectAccount,
|
||||
} = await generateConfigsAsync(args, actor, graphqlClient);
|
||||
|
||||
const projectDirectory = await cloneTemplateAsync(targetProjectDirectory);
|
||||
|
||||
const packageManager = flags['package-manager'];
|
||||
await installProjectDependenciesAsync(projectDirectory, packageManager);
|
||||
|
||||
const projectId = await createProjectAsync({
|
||||
projectDirectory,
|
||||
projectAccount,
|
||||
projectName,
|
||||
actor,
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
await generateProjectFilesAsync(projectDirectory, app, packageManager);
|
||||
|
||||
await initializeGitRepositoryAsync(projectDirectory);
|
||||
|
||||
Log.log('🎉 We finished creating your new project.');
|
||||
Log.newLine();
|
||||
Log.log('Next steps:');
|
||||
Log.withInfo(`Run \`cd ${printDirectory(projectDirectory)}\` to navigate to your project.`);
|
||||
Log.withInfo(
|
||||
`Run \`${packageManager} run draft\` to create a preview on EAS. ${learnMore(
|
||||
'https://docs.expo.dev/eas/workflows/examples/publish-preview-update/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Run \`${packageManager} run start\` to start developing locally. ${learnMore(
|
||||
'https://docs.expo.dev/get-started/start-developing/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(`See the README.md for more information about your project.`);
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export default class Onboarding extends EasCommand {
|
||||
}))
|
||||
? 'ssh'
|
||||
: 'https';
|
||||
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preffered git clone method`));
|
||||
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
|
||||
Log.log();
|
||||
|
||||
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
AndroidSubmissionConfigInput,
|
||||
IosSubmissionConfigInput,
|
||||
SubmissionAndroidReleaseStatus,
|
||||
SubmissionAndroidTrack,
|
||||
} from '../../graphql/generated';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import { GoogleServiceAccountKeyQuery } from '../../graphql/queries/GoogleServiceAccountKeyQuery';
|
||||
@@ -141,12 +140,11 @@ export default class SubmitInternal extends EasCommand {
|
||||
graphqlClient,
|
||||
})
|
||||
);
|
||||
const track = graphQlTrackToConfigTrack[androidConfig.track] ?? androidConfig.track;
|
||||
|
||||
const configInput: z.input<typeof SubmissionConfig.Android.SchemaZ> = {
|
||||
changesNotSentForReview,
|
||||
googleServiceAccountKeyJson,
|
||||
track,
|
||||
track: androidConfig.track,
|
||||
...(releaseStatus === SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS
|
||||
? {
|
||||
releaseStatus: SubmissionConfig.Android.ReleaseStatus.IN_PROGRESS,
|
||||
@@ -217,10 +215,3 @@ const graphQlReleaseStatusToConfigReleaseStatus = {
|
||||
[SubmissionAndroidReleaseStatus.Completed]: SubmissionConfig.Android.ReleaseStatus.COMPLETED,
|
||||
[SubmissionAndroidReleaseStatus.Halted]: SubmissionConfig.Android.ReleaseStatus.HALTED,
|
||||
};
|
||||
|
||||
const graphQlTrackToConfigTrack = {
|
||||
[SubmissionAndroidTrack.Production]: SubmissionConfig.Android.ReleaseTrack.PRODUCTION,
|
||||
[SubmissionAndroidTrack.Beta]: SubmissionConfig.Android.ReleaseTrack.BETA,
|
||||
[SubmissionAndroidTrack.Alpha]: SubmissionConfig.Android.ReleaseTrack.ALPHA,
|
||||
[SubmissionAndroidTrack.Internal]: SubmissionConfig.Android.ReleaseTrack.INTERNAL,
|
||||
};
|
||||
@@ -39,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: flags['environment'],
|
||||
withServerSideEnvironment: flags['environment'] ?? null,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
|
||||
@@ -4,9 +4,8 @@ import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
// import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { transformFingerprintSource } from '../../build/graphql';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -15,7 +14,6 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
AssetMapSourceInput,
|
||||
EnvironmentVariableEnvironment,
|
||||
FingerprintInfoGroup as GraphqlFingerprintInfoGroup,
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
@@ -24,7 +22,7 @@ import {
|
||||
UpdateRolloutInfoGroup,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
// import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
@@ -38,7 +36,7 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
filterCollectedAssetsByRequestedPlatforms,
|
||||
findCompatibleBuildsAsync,
|
||||
// findCompatibleBuildsAsync,
|
||||
generateEasMetadataAsync,
|
||||
getBranchNameForCommandAsync,
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
|
||||
@@ -80,7 +78,7 @@ type RawUpdateFlags = {
|
||||
'rollout-percentage'?: number;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
environment: EnvironmentVariableEnvironment | null;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
@@ -97,7 +95,7 @@ type UpdateFlags = {
|
||||
rolloutPercentage?: number;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
environment: EnvironmentVariableEnvironment | null;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
@@ -194,7 +192,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
} = await this.getContextAsync(UpdatePublish, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: environment,
|
||||
withServerSideEnvironment: environment ?? null,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
@@ -488,10 +486,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
(prev, [platform, fingerprintInfo]) => {
|
||||
return {
|
||||
...prev,
|
||||
[platform]: {
|
||||
...fingerprintInfo,
|
||||
fingerprintSource: transformFingerprintSource(fingerprintInfo.fingerprintSource),
|
||||
},
|
||||
[platform]: fingerprintInfo,
|
||||
};
|
||||
},
|
||||
{} as GraphqlFingerprintInfoGroup
|
||||
@@ -594,11 +589,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
const runtimeToCompatibleBuilds = await Promise.all(
|
||||
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
|
||||
findCompatibleBuildsAsync(graphqlClient, projectId, obj)
|
||||
)
|
||||
);
|
||||
// const runtimeToCompatibleBuilds = await Promise.all(
|
||||
// runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(obj =>
|
||||
// findCompatibleBuildsAsync(graphqlClient, projectId, obj)
|
||||
// )
|
||||
// );
|
||||
|
||||
for (const runtime of uniqBy(
|
||||
runtimeToPlatformsAndFingerprintInfoMapping,
|
||||
@@ -672,52 +667,56 @@ export default class UpdatePublish extends EasCommand {
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
|
||||
const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
|
||||
({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
|
||||
)?.fingerprintInfoGroupWithCompatibleBuilds;
|
||||
if (fingerprintsWithoutCompatibleBuilds) {
|
||||
const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
|
||||
([_platform, fingerprintInfo]) => !fingerprintInfo.build
|
||||
);
|
||||
if (missingBuilds.length > 0) {
|
||||
const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
Log.warn('No compatible builds found for the following fingerprints:');
|
||||
for (const [platform, fingerprintInfo] of missingBuilds) {
|
||||
const fingerprintUrl = new URL(
|
||||
`/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
);
|
||||
Log.warn(
|
||||
formatFields(
|
||||
[
|
||||
{
|
||||
label: `${this.prettyPlatform(platform)} fingerprint`,
|
||||
value: fingerprintInfo.fingerprintHash,
|
||||
},
|
||||
{ label: 'URL', value: fingerprintUrl.toString() },
|
||||
],
|
||||
{
|
||||
labelFormat: label => ` ${chalk.dim(label)}:`,
|
||||
}
|
||||
)
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE(brentvatne): temporarily disable logging this until we can revisit the formatting
|
||||
// and the logic for it - it's a bit too aggressive right now, and warns even if you're
|
||||
// not using EAS Build
|
||||
//
|
||||
// const fingerprintsWithoutCompatibleBuilds = runtimeToCompatibleBuilds.find(
|
||||
// ({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion
|
||||
// )?.fingerprintInfoGroupWithCompatibleBuilds;
|
||||
// if (fingerprintsWithoutCompatibleBuilds) {
|
||||
// const missingBuilds = Object.entries(fingerprintsWithoutCompatibleBuilds).filter(
|
||||
// ([_platform, fingerprintInfo]) => !fingerprintInfo.build
|
||||
// );
|
||||
// if (missingBuilds.length > 0) {
|
||||
// const project = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
// Log.warn('No compatible builds found for the following fingerprints:');
|
||||
// for (const [platform, fingerprintInfo] of missingBuilds) {
|
||||
// const fingerprintUrl = new URL(
|
||||
// `/accounts/${project.ownerAccount.name}/projects/${project.slug}/fingerprints/${fingerprintInfo.fingerprintHash}`,
|
||||
// getExpoWebsiteBaseUrl()
|
||||
// );
|
||||
// Log.warn(
|
||||
// formatFields(
|
||||
// [
|
||||
// {
|
||||
// label: `${this.prettyPlatform(platform)} fingerprint`,
|
||||
// value: fingerprintInfo.fingerprintHash,
|
||||
// },
|
||||
// { label: 'URL', value: fingerprintUrl.toString() },
|
||||
// ],
|
||||
// {
|
||||
// labelFormat: label => ` ${chalk.dim(label)}:`,
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// Log.addNewLineIfNone();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private prettyPlatform(updatePlatform: string): string {
|
||||
switch (updatePlatform) {
|
||||
case 'android':
|
||||
return 'Android';
|
||||
case 'ios':
|
||||
return 'iOS';
|
||||
default:
|
||||
return updatePlatform;
|
||||
}
|
||||
}
|
||||
// private prettyPlatform(updatePlatform: string): string {
|
||||
// switch (updatePlatform) {
|
||||
// case 'android':
|
||||
// return 'Android';
|
||||
// case 'ios':
|
||||
// return 'iOS';
|
||||
// default:
|
||||
// return updatePlatform;
|
||||
// }
|
||||
// }
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { parseInputs, parseJsonInputs, parseWorkflowInputsFromYaml } from '../run';
|
||||
import {
|
||||
parseInputs,
|
||||
parseJsonInputs,
|
||||
parseWorkflowInputsFromYaml,
|
||||
} from '../../../commandUtils/workflow/inputs';
|
||||
|
||||
describe('parseInputs', () => {
|
||||
it('should parse single key=value pair', () => {
|
||||
@@ -216,7 +220,6 @@ jobs:
|
||||
it('should handle invalid YAML gracefully', () => {
|
||||
const yamlConfig = 'invalid: yaml: content:';
|
||||
|
||||
const { parseWorkflowInputsFromYaml } = require('../run');
|
||||
const inputs = parseWorkflowInputsFromYaml(yamlConfig);
|
||||
|
||||
expect(inputs).toEqual({});
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs/promises';
|
||||
import fsExtra from 'fs-extra';
|
||||
import path from 'path';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
runBuildConfigureIfNeededAsync,
|
||||
runUpdateConfigureIfNeededAsync,
|
||||
} from '../../commandUtils/workflow/buildProfileUtils';
|
||||
import {
|
||||
WorkflowStarter,
|
||||
WorkflowStarterName,
|
||||
customizeTemplateIfNeededAsync,
|
||||
howToRunWorkflow,
|
||||
workflowStarters,
|
||||
} from '../../commandUtils/workflow/creation';
|
||||
import {
|
||||
logWorkflowValidationErrors,
|
||||
validateWorkflowFileAsync,
|
||||
workflowContentsFromParsedYaml,
|
||||
} from '../../commandUtils/workflow/validation';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME = 'workflow.yml';
|
||||
const HELLO_WORLD_TEMPLATE = `name: Hello World
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
hello_world:
|
||||
steps:
|
||||
- uses: eas/checkout
|
||||
- run: echo "Hello, World"
|
||||
`;
|
||||
|
||||
export class WorkflowCreate extends EasCommand {
|
||||
static override description = 'create a new workflow configuration YAML file';
|
||||
|
||||
@@ -35,11 +38,16 @@ export class WorkflowCreate extends EasCommand {
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
'skip-validation': Flags.boolean({
|
||||
description: 'If set, the workflow file will not be validated before being created',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -48,74 +56,175 @@ export class WorkflowCreate extends EasCommand {
|
||||
flags,
|
||||
} = await this.parse(WorkflowCreate);
|
||||
|
||||
const { projectDir } = await this.getContextAsync(WorkflowCreate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
});
|
||||
try {
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(WorkflowCreate, {
|
||||
nonInteractive: false,
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
let fileName = argFileName;
|
||||
const { exp: originalExpoConfig, projectId } = await getDynamicPrivateProjectConfigAsync();
|
||||
let expoConfig = originalExpoConfig;
|
||||
|
||||
if (!fileName) {
|
||||
const response = await prompts({
|
||||
let workflowStarter;
|
||||
while (!workflowStarter) {
|
||||
workflowStarter = await chooseTemplateAsync();
|
||||
switch (workflowStarter.name) {
|
||||
case WorkflowStarterName.BUILD:
|
||||
case WorkflowStarterName.DEPLOY:
|
||||
case WorkflowStarterName.UPDATE: {
|
||||
const shouldProceed = await runBuildConfigureIfNeededAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
});
|
||||
if (!shouldProceed) {
|
||||
workflowStarter = undefined;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (workflowStarter.name) {
|
||||
case WorkflowStarterName.DEPLOY:
|
||||
case WorkflowStarterName.UPDATE: {
|
||||
const shouldProceed = await runUpdateConfigureIfNeededAsync({
|
||||
projectDir,
|
||||
expoConfig,
|
||||
});
|
||||
if (!shouldProceed) {
|
||||
workflowStarter = undefined;
|
||||
continue;
|
||||
}
|
||||
// Need to refetch the Expo config because it may have changed
|
||||
expoConfig = (await getDynamicPrivateProjectConfigAsync()).exp;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const { fileName, filePath } = await chooseFileNameAsync(
|
||||
argFileName,
|
||||
projectDir,
|
||||
workflowStarter
|
||||
);
|
||||
|
||||
// Customize the template if needed
|
||||
workflowStarter = await customizeTemplateIfNeededAsync(
|
||||
workflowStarter,
|
||||
projectDir,
|
||||
expoConfig
|
||||
);
|
||||
|
||||
Log.debug(`Creating workflow file ${fileName} from template ${workflowStarter.name}`);
|
||||
const yamlString = [
|
||||
workflowStarter.header,
|
||||
workflowContentsFromParsedYaml(workflowStarter.template),
|
||||
].join('\n');
|
||||
|
||||
if (!flags['skip-validation']) {
|
||||
await validateWorkflowFileAsync(
|
||||
{ yamlConfig: yamlString, filePath: fileName },
|
||||
projectDir,
|
||||
graphqlClient,
|
||||
projectId
|
||||
);
|
||||
}
|
||||
await ensureWorkflowsDirectoryExistsAsync({ projectDir });
|
||||
filePath && (await fs.writeFile(filePath, yamlString));
|
||||
Log.withTick(`Created ${chalk.bold(filePath)}`);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(howToRunWorkflow(fileName, workflowStarter));
|
||||
|
||||
// Next steps
|
||||
if (workflowStarter.nextSteps && workflowStarter.nextSteps.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Next steps:');
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
formatFields(
|
||||
workflowStarter.nextSteps.map((step: string, index: number) => ({
|
||||
label: `${index + 1}.`,
|
||||
value: step,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logWorkflowValidationErrors(error);
|
||||
Log.error('Failed to create workflow file.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWorkflowsDirectoryExistsAsync({
|
||||
projectDir,
|
||||
}: {
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await fs.access(path.join(projectDir, '.eas', 'workflows'));
|
||||
} catch {
|
||||
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
|
||||
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseTemplateAsync(): Promise<WorkflowStarter> {
|
||||
const workflowStarter = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'starter',
|
||||
message: 'Select a workflow template:',
|
||||
choices: workflowStarters.map(starter => ({
|
||||
title: starter.displayName,
|
||||
value: starter,
|
||||
})),
|
||||
})
|
||||
).starter;
|
||||
return workflowStarter;
|
||||
}
|
||||
|
||||
async function chooseFileNameAsync(
|
||||
initialValue: string | undefined,
|
||||
projectDir: string,
|
||||
workflowStarter: WorkflowStarter
|
||||
): Promise<{ fileName: string; filePath: string }> {
|
||||
let fileName = initialValue;
|
||||
let filePath = '';
|
||||
while ((fileName?.length ?? 0) === 0) {
|
||||
fileName = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'fileName',
|
||||
message: 'What would you like to name your workflow file?',
|
||||
initial: DEFAULT_WORKFLOW_NAME,
|
||||
validate: value => {
|
||||
try {
|
||||
WorkflowFile.validateYamlExtension(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : 'Invalid file name';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.fileName) {
|
||||
Log.warn('Workflow creation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fileName = response.fileName;
|
||||
initial: workflowStarter.defaultFileName,
|
||||
})
|
||||
).fileName;
|
||||
if (!fileName) {
|
||||
fileName = undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ensureWorkflowsDirectoryExistsAsync({ projectDir });
|
||||
await this.createWorkflowFileAsync({ fileName, projectDir });
|
||||
WorkflowFile.validateYamlExtension(fileName);
|
||||
} catch (error) {
|
||||
Log.error('Failed to create workflow file.');
|
||||
throw error;
|
||||
Log.error(error instanceof Error ? error.message : 'Invalid YAML file name extension');
|
||||
fileName = undefined;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureWorkflowsDirectoryExistsAsync({
|
||||
projectDir,
|
||||
}: {
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await fs.access(path.join(projectDir, '.eas', 'workflows'));
|
||||
} catch {
|
||||
await fs.mkdir(path.join(projectDir, '.eas', 'workflows'), { recursive: true });
|
||||
Log.withTick(`Created directory ${chalk.bold(path.join(projectDir, '.eas', 'workflows'))}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async createWorkflowFileAsync({
|
||||
fileName,
|
||||
projectDir,
|
||||
}: {
|
||||
fileName: string;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
WorkflowFile.validateYamlExtension(fileName);
|
||||
|
||||
const filePath = path.join(projectDir, '.eas', 'workflows', fileName);
|
||||
|
||||
filePath = path.join(projectDir, '.eas', 'workflows', fileName);
|
||||
if (await fsExtra.pathExists(filePath)) {
|
||||
throw new Error(`Workflow file already exists: ${filePath}`);
|
||||
Log.error(`Workflow file already exists: ${filePath}`);
|
||||
Log.error('Please choose a different file name.');
|
||||
Log.newLine();
|
||||
fileName = undefined;
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, HELLO_WORLD_TEMPLATE);
|
||||
Log.withTick(`Created ${chalk.bold(filePath)}`);
|
||||
}
|
||||
return { fileName: fileName ?? '', filePath };
|
||||
}
|
||||
@@ -40,21 +40,33 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'node:fs';
|
||||
import { boolish } from 'getenv';
|
||||
import * as path from 'node:path';
|
||||
import slash from 'slash';
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
maybePromptForMissingInputsAsync,
|
||||
parseInputs,
|
||||
parseJsonInputs,
|
||||
parseWorkflowInputsFromYaml,
|
||||
} from '../../commandUtils/workflow/inputs';
|
||||
import {
|
||||
fileExistsAsync,
|
||||
infoForActiveWorkflowRunAsync,
|
||||
infoForFailedWorkflowRunAsync,
|
||||
maybeReadStdinAsync,
|
||||
} from '../../commandUtils/workflow/utils';
|
||||
import {
|
||||
WorkflowProjectSourceType,
|
||||
WorkflowRevision,
|
||||
WorkflowRunByIdQuery,
|
||||
WorkflowRunStatus,
|
||||
} from '../../graphql/generated';
|
||||
@@ -66,7 +78,6 @@ import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
@@ -78,7 +89,8 @@ const EXIT_CODES = {
|
||||
};
|
||||
|
||||
export default class WorkflowRun extends EasCommand {
|
||||
static override description = 'run an EAS workflow';
|
||||
static override description =
|
||||
'run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow run, unless the --ref flag is used.';
|
||||
|
||||
static override args = [{ name: 'file', description: 'Path to the workflow file to run' }];
|
||||
|
||||
@@ -88,7 +100,7 @@ export default class WorkflowRun extends EasCommand {
|
||||
default: false,
|
||||
allowNo: true,
|
||||
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
|
||||
summary: 'Wait for workflow run to complete',
|
||||
summary: 'Wait for workflow run to complete. Defaults to false.',
|
||||
}),
|
||||
input: Flags.string({
|
||||
char: 'F',
|
||||
@@ -98,6 +110,11 @@ export default class WorkflowRun extends EasCommand {
|
||||
'Add a parameter in key=value format. Use multiple instances of this flag to set multiple inputs.',
|
||||
summary: 'Set workflow inputs',
|
||||
}),
|
||||
ref: Flags.string({
|
||||
description:
|
||||
"The git reference must exist in the project's git repository, and the workflow file must exist at that reference. When this flag is used, the local project is not uploaded; instead, the workflow is run from the exact state of the project at the chosen reference.",
|
||||
summary: 'Git reference to run the workflow on',
|
||||
}),
|
||||
...EasJsonOnlyFlag,
|
||||
};
|
||||
|
||||
@@ -125,26 +142,70 @@ export default class WorkflowRun extends EasCommand {
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
let yamlConfig: string;
|
||||
try {
|
||||
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
|
||||
projectDir,
|
||||
filePath: args.file,
|
||||
});
|
||||
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
|
||||
yamlConfig = workflowFileContents.yamlConfig;
|
||||
} catch (err) {
|
||||
Log.error('Failed to read workflow file.');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
exp: { slug: projectName },
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
let yamlConfig: string;
|
||||
let workflowRunId: string;
|
||||
let workflowRevisionId: string | undefined;
|
||||
let gitRef: string | undefined;
|
||||
|
||||
if (flags.ref) {
|
||||
// Run from git ref
|
||||
const fileName = path.basename(args.file);
|
||||
// Find the real commit, make sure the ref is valid
|
||||
gitRef = (
|
||||
await spawnAsync('git', ['rev-parse', flags.ref], {
|
||||
cwd: projectDir,
|
||||
})
|
||||
).output[0].trim();
|
||||
if (!gitRef) {
|
||||
throw new Error('Failed to resolve git reference');
|
||||
}
|
||||
Log.log(`Using workflow file ${fileName} at ${gitRef}`);
|
||||
let revisionResult: WorkflowRevision | undefined;
|
||||
try {
|
||||
revisionResult = await WorkflowRevisionMutation.getOrCreateWorkflowRevisionFromGitRefAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: projectId,
|
||||
fileName,
|
||||
gitRef,
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to find or create workflow revision for ${fileName} at ${flags.ref}: ${err}`
|
||||
);
|
||||
}
|
||||
Log.debug(`Workflow revision: ${JSON.stringify(revisionResult, null, 2)}`);
|
||||
if (!revisionResult) {
|
||||
throw new Error(
|
||||
`Failed to find or create workflow revision for ${fileName} at ${flags.ref}`
|
||||
);
|
||||
}
|
||||
yamlConfig = revisionResult.yamlConfig;
|
||||
workflowRevisionId = revisionResult.id;
|
||||
} else {
|
||||
// Run from local file
|
||||
try {
|
||||
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
|
||||
projectDir,
|
||||
filePath: args.file,
|
||||
});
|
||||
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
|
||||
yamlConfig = workflowFileContents.yamlConfig;
|
||||
} catch (err) {
|
||||
Log.error('Failed to read workflow file.');
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate workflow YAML
|
||||
try {
|
||||
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
@@ -154,8 +215,6 @@ export default class WorkflowRun extends EasCommand {
|
||||
if (error instanceof CombinedError) {
|
||||
WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
||||
error,
|
||||
accountName: account.name,
|
||||
projectName,
|
||||
});
|
||||
|
||||
throw error;
|
||||
@@ -208,81 +267,95 @@ export default class WorkflowRun extends EasCommand {
|
||||
path.relative(await vcsClient.getRootPathAsync(), projectDir) || '.'
|
||||
);
|
||||
|
||||
try {
|
||||
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
|
||||
graphqlClient,
|
||||
vcsClient,
|
||||
accountId: account.id,
|
||||
}));
|
||||
|
||||
if (await fileExistsAsync(easJsonPath)) {
|
||||
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: easJsonPath,
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
} else {
|
||||
Log.warn(
|
||||
`⚠ No ${chalk.bold('eas.json')} found in the project directory. Running ${chalk.bold(
|
||||
'type: build'
|
||||
)} jobs will not work. Run ${chalk.bold(
|
||||
'eas build:configure'
|
||||
)} to configure your project for builds.`
|
||||
);
|
||||
}
|
||||
|
||||
if (await fileExistsAsync(packageJsonPath)) {
|
||||
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: packageJsonPath,
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
} else {
|
||||
Log.warn(
|
||||
`⚠ No ${chalk.bold(
|
||||
'package.json'
|
||||
)} found in the project directory. It is used to automatically infer best job configuration for your project. You may want to define ${chalk.bold(
|
||||
'image'
|
||||
)} property in your workflow to specify the image to use.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error('Failed to upload project sources.');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
let workflowRunId: string;
|
||||
|
||||
try {
|
||||
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
workflowRevisionInput: {
|
||||
fileName: path.basename(args.file),
|
||||
yamlConfig,
|
||||
},
|
||||
workflowRunInput: {
|
||||
if (gitRef) {
|
||||
// Run from git ref
|
||||
let runResult: { id: string };
|
||||
try {
|
||||
runResult = await WorkflowRunMutation.createWorkflowRunFromGitRefAsync(graphqlClient, {
|
||||
workflowRevisionId: workflowRevisionId ?? '',
|
||||
gitRef,
|
||||
inputs,
|
||||
projectSource: {
|
||||
type: WorkflowProjectSourceType.Gcs,
|
||||
projectArchiveBucketKey,
|
||||
easJsonBucketKey,
|
||||
packageJsonBucketKey,
|
||||
projectRootDirectory,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to create workflow run: ${err}`);
|
||||
}
|
||||
workflowRunId = runResult.id;
|
||||
} else {
|
||||
// Run from local file
|
||||
try {
|
||||
({ projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
|
||||
graphqlClient,
|
||||
vcsClient,
|
||||
accountId: account.id,
|
||||
}));
|
||||
|
||||
if (await fileExistsAsync(easJsonPath)) {
|
||||
({ fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: easJsonPath,
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
} else {
|
||||
Log.warn(
|
||||
`⚠ No ${chalk.bold('eas.json')} found in the project directory. Running ${chalk.bold(
|
||||
'type: build'
|
||||
)} jobs will not work. Run ${chalk.bold(
|
||||
'eas build:configure'
|
||||
)} to configure your project for builds.`
|
||||
);
|
||||
}
|
||||
|
||||
if (await fileExistsAsync(packageJsonPath)) {
|
||||
({ fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: packageJsonPath,
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
}));
|
||||
} else {
|
||||
Log.warn(
|
||||
`⚠ No ${chalk.bold(
|
||||
'package.json'
|
||||
)} found in the project directory. It is used to automatically infer best job configuration for your project. You may want to define ${chalk.bold(
|
||||
'image'
|
||||
)} property in your workflow to specify the image to use.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error('Failed to upload project sources.');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
({ id: workflowRunId } = await WorkflowRunMutation.createWorkflowRunAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
workflowRevisionInput: {
|
||||
fileName: path.basename(args.file),
|
||||
yamlConfig,
|
||||
},
|
||||
},
|
||||
}));
|
||||
workflowRunInput: {
|
||||
inputs,
|
||||
projectSource: {
|
||||
type: WorkflowProjectSourceType.Gcs,
|
||||
projectArchiveBucketKey,
|
||||
easJsonBucketKey,
|
||||
packageJsonBucketKey,
|
||||
projectRootDirectory,
|
||||
},
|
||||
},
|
||||
}));
|
||||
} catch (err) {
|
||||
Log.error('Failed to start the workflow with the API.');
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
|
||||
} catch (err) {
|
||||
Log.error('Failed to start the workflow with the API.');
|
||||
|
||||
throw err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
|
||||
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput({
|
||||
@@ -294,9 +367,11 @@ export default class WorkflowRun extends EasCommand {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
const spinnerUsesStdErr = boolish('CI', false) || flags.json;
|
||||
|
||||
const { status } = await waitForWorkflowRunToEndAsync(graphqlClient, {
|
||||
workflowRunId,
|
||||
spinnerUsesStdErr,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
@@ -320,40 +395,53 @@ export default class WorkflowRun extends EasCommand {
|
||||
|
||||
async function waitForWorkflowRunToEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ workflowRunId }: { workflowRunId: string }
|
||||
{ workflowRunId, spinnerUsesStdErr }: { workflowRunId: string; spinnerUsesStdErr: boolean }
|
||||
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
|
||||
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
|
||||
|
||||
const spinner = ora('Currently waiting for workflow run to start.').start();
|
||||
const spinner = ora({
|
||||
stream: spinnerUsesStdErr ? process.stderr : process.stdout,
|
||||
text: '',
|
||||
}).start();
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting to start:}`;
|
||||
|
||||
let failedFetchesCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const workflowRun = await WorkflowRunQuery.byIdAsync(graphqlClient, workflowRunId, {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
|
||||
failedFetchesCount = 0;
|
||||
|
||||
switch (workflowRun.status) {
|
||||
case WorkflowRunStatus.InProgress:
|
||||
spinner.start('Workflow run is in progress.');
|
||||
case WorkflowRunStatus.New:
|
||||
break;
|
||||
|
||||
case WorkflowRunStatus.InProgress: {
|
||||
spinner.prefixText = chalk`{bold.green Workflow run is in progress:}`;
|
||||
spinner.text = await infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, 5);
|
||||
break;
|
||||
}
|
||||
case WorkflowRunStatus.ActionRequired:
|
||||
spinner.warn('Workflow run is waiting for action.');
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting for action:}`;
|
||||
break;
|
||||
|
||||
case WorkflowRunStatus.Canceled:
|
||||
spinner.warn('Workflow run has been canceled.');
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow has been canceled.}`;
|
||||
spinner.stopAndPersist();
|
||||
return workflowRun;
|
||||
|
||||
case WorkflowRunStatus.Failure:
|
||||
spinner.fail('Workflow run has failed.');
|
||||
case WorkflowRunStatus.Failure: {
|
||||
spinner.prefixText = chalk`{bold.red Workflow has failed.}`;
|
||||
const failedInfo = await infoForFailedWorkflowRunAsync(graphqlClient, workflowRun);
|
||||
spinner.fail(failedInfo);
|
||||
return workflowRun;
|
||||
}
|
||||
case WorkflowRunStatus.Success:
|
||||
spinner.succeed('Workflow run completed successfully.');
|
||||
spinner.prefixText = chalk`{bold.green Workflow has completed successfully.}`;
|
||||
spinner.text = '';
|
||||
spinner.succeed('');
|
||||
return workflowRun;
|
||||
}
|
||||
} catch {
|
||||
@@ -370,253 +458,3 @@ async function waitForWorkflowRunToEndAsync(
|
||||
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExistsAsync(filePath: string): Promise<boolean> {
|
||||
return await fs.promises
|
||||
.access(filePath, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
export function parseInputs(inputFlags: string[]): Record<string, string> {
|
||||
const inputs: Record<string, string> = {};
|
||||
|
||||
for (const inputFlag of inputFlags) {
|
||||
const equalIndex = inputFlag.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
|
||||
}
|
||||
|
||||
const key = inputFlag.substring(0, equalIndex);
|
||||
const value = inputFlag.substring(equalIndex + 1);
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
|
||||
}
|
||||
|
||||
inputs[key] = value;
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export async function maybeReadStdinAsync(): Promise<string | null> {
|
||||
// Check if there's data on stdin
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const trimmedData = data.trim();
|
||||
resolve(trimmedData || null);
|
||||
});
|
||||
|
||||
process.stdin.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function parseJsonInputs(jsonString: string): Record<string, string> {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('JSON input must be an object.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input.`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
|
||||
// which is not what we want. See:
|
||||
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
|
||||
const booleanLike = z.union([
|
||||
z.boolean(),
|
||||
z.number().pipe(z.coerce.boolean()),
|
||||
z.preprocess(val => {
|
||||
if (typeof val === 'string') {
|
||||
if (val.toLowerCase() === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (val.toLowerCase() === 'false') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
}, z.boolean()),
|
||||
]);
|
||||
|
||||
const stringLike = z
|
||||
.union([
|
||||
// We're going to coerce numbers and strings into strings.
|
||||
z.number(),
|
||||
z.string(),
|
||||
// We do not allow other primitives, like:
|
||||
// - bigints, symbols - because YAML does not support them,
|
||||
// - booleans - because YAML accepts `True` and `true` as boolean input
|
||||
// and parses both as JS `true` -- if we stringified that,
|
||||
// we would lose the capital "T" which may not be what the user expects,
|
||||
// - nulls - user should do `"null"` or not pass the property at all.
|
||||
])
|
||||
.pipe(z.coerce.string());
|
||||
|
||||
export const WorkflowDispatchInputZ = z
|
||||
.object({
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
required: booleanLike.default(false).describe('Whether the input is required'),
|
||||
})
|
||||
.and(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal('string').default('string'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('boolean'),
|
||||
default: booleanLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('choice'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('environment'),
|
||||
default: z.string().optional().describe('Default value for the input'),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
.parse(parsed?.on?.workflow_dispatch?.inputs);
|
||||
} catch (error) {
|
||||
Log.warn('Failed to parse workflow inputs from YAML:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function maybePromptForMissingInputsAsync({
|
||||
inputSpecs,
|
||||
inputs,
|
||||
}: {
|
||||
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
|
||||
inputs: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
|
||||
|
||||
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
|
||||
|
||||
if (missingRequiredInputs.length === 0) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Some required inputs are missing. Please provide them:');
|
||||
|
||||
const nextInputs = { ...inputs };
|
||||
|
||||
for (const [key, spec] of missingRequiredInputs) {
|
||||
const value = await promptForMissingInputAsync({ key, spec });
|
||||
nextInputs[key] = value;
|
||||
}
|
||||
|
||||
return nextInputs;
|
||||
}
|
||||
|
||||
async function promptForMissingInputAsync({
|
||||
key,
|
||||
spec,
|
||||
}: {
|
||||
key: string;
|
||||
spec: z.infer<typeof WorkflowDispatchInputZ>;
|
||||
}): Promise<unknown> {
|
||||
const message = spec.description ? `${key} (${spec.description})` : key;
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'number',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: number) => {
|
||||
if (isNaN(val)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'choice': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'value',
|
||||
message,
|
||||
choices: spec.options.map(option => ({
|
||||
title: option,
|
||||
value: option,
|
||||
})),
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'environment': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: string) => {
|
||||
if (spec.required && (!val || val.trim() === '')) {
|
||||
return 'This field is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { CombinedError } from '@urql/core';
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
|
||||
import {
|
||||
logWorkflowValidationErrors,
|
||||
validateWorkflowFileAsync,
|
||||
} from '../../commandUtils/workflow/validation';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
export class WorkflowValidate extends EasCommand {
|
||||
@@ -27,6 +26,7 @@ export class WorkflowValidate extends EasCommand {
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
@@ -36,67 +36,30 @@ export class WorkflowValidate extends EasCommand {
|
||||
flags,
|
||||
} = await this.parse(WorkflowValidate);
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(WorkflowValidate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const {
|
||||
projectId,
|
||||
exp: { slug: projectName },
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
const spinner = ora().start('Validating the workflow YAML file…');
|
||||
|
||||
try {
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
projectDir,
|
||||
projectId,
|
||||
} = await this.getContextAsync(WorkflowValidate, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const workflowFileContents = await WorkflowFile.readWorkflowFileContentsAsync({
|
||||
projectDir,
|
||||
filePath,
|
||||
});
|
||||
Log.log(`Using workflow file from ${workflowFileContents.filePath}`);
|
||||
|
||||
const parsedYaml = YAML.parse(workflowFileContents.yamlConfig);
|
||||
|
||||
// Check if the parsed result is empty or null
|
||||
if (
|
||||
parsedYaml === null ||
|
||||
parsedYaml === undefined ||
|
||||
(typeof parsedYaml === 'object' && Object.keys(parsedYaml).length === 0)
|
||||
) {
|
||||
throw new Error('YAML file is empty or contains only comments.');
|
||||
}
|
||||
|
||||
await WorkflowRevisionMutation.validateWorkflowYamlConfigAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
yamlConfig: workflowFileContents.yamlConfig,
|
||||
});
|
||||
await validateWorkflowFileAsync(workflowFileContents, projectDir, graphqlClient, projectId);
|
||||
|
||||
spinner.succeed('Workflow configuration YAML is valid.');
|
||||
} catch (error) {
|
||||
spinner.fail('Workflow configuration YAML is not valid.');
|
||||
|
||||
if (error instanceof YAML.YAMLParseError) {
|
||||
Log.error(`YAML syntax error: ${error.message}`);
|
||||
} else if (error instanceof CombinedError) {
|
||||
WorkflowFile.maybePrintWorkflowFileValidationErrors({
|
||||
error,
|
||||
accountName: account.name,
|
||||
projectName,
|
||||
});
|
||||
|
||||
throw error;
|
||||
} else if (error instanceof Error) {
|
||||
Log.error(error.message);
|
||||
} else {
|
||||
Log.error(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
logWorkflowValidationErrors(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { formatGraphQLBuildArtifacts } from '../../build/utils/formatBuild';
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
@@ -7,12 +10,52 @@ import {
|
||||
} from '../../commandUtils/workflow/stateMachine';
|
||||
import { WorkflowTriggerType } from '../../commandUtils/workflow/types';
|
||||
import { computeTriggerInfoForWorkflowRun } from '../../commandUtils/workflow/utils';
|
||||
import { WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
|
||||
import {
|
||||
BuildArtifacts,
|
||||
WorkflowArtifact,
|
||||
WorkflowJobType,
|
||||
WorkflowRunByIdWithJobsQuery,
|
||||
} from '../../graphql/generated';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
type ReducedWorkflowArtifact = Omit<
|
||||
WorkflowArtifact,
|
||||
'createdAt' | 'jobRun' | 'storageType' | 'updatedAt'
|
||||
>;
|
||||
|
||||
type WorkflowJobOutput =
|
||||
WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number]['outputs'];
|
||||
|
||||
type WorkflowJobResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] & {
|
||||
artifacts?: ReducedWorkflowArtifact[] | BuildArtifacts | undefined;
|
||||
output?: WorkflowJobOutput | undefined;
|
||||
};
|
||||
|
||||
type WorkflowRunResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] & {
|
||||
logURL?: string | undefined;
|
||||
triggerType?: WorkflowTriggerType | undefined;
|
||||
trigger?: string | null;
|
||||
jobs: WorkflowJobResult[];
|
||||
};
|
||||
|
||||
const processedOutputs: (job: WorkflowJobResult) => FormatFieldsItem[] = job => {
|
||||
const result: FormatFieldsItem[] = [];
|
||||
const keys = job.outputs ? Object.keys(job.outputs) : [];
|
||||
keys.forEach(key => {
|
||||
result.push({
|
||||
label: ` ${key}`,
|
||||
get value(): string {
|
||||
const value = job.outputs[key];
|
||||
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
},
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export default class WorkflowView extends EasCommand {
|
||||
static override description =
|
||||
'view details for a workflow run, including jobs. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.';
|
||||
@@ -60,11 +103,6 @@ export default class WorkflowView extends EasCommand {
|
||||
}
|
||||
const idToQuery = actionResult.runId ?? '';
|
||||
|
||||
type WorkflowRunResult = WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] & {
|
||||
logURL?: string;
|
||||
triggerType?: WorkflowTriggerType;
|
||||
trigger?: string | null;
|
||||
};
|
||||
const result: WorkflowRunResult = await WorkflowRunQuery.withJobsByIdAsync(
|
||||
graphqlClient,
|
||||
idToQuery,
|
||||
@@ -76,9 +114,18 @@ export default class WorkflowView extends EasCommand {
|
||||
result.triggerType = triggerType;
|
||||
result.trigger = trigger;
|
||||
|
||||
result.jobs.forEach(job => {
|
||||
delete job.turtleJobRun;
|
||||
const processedJobs: WorkflowJobResult[] = result.jobs.map(job => {
|
||||
const processedJob = job as WorkflowJobResult;
|
||||
if (job.type === WorkflowJobType.Build) {
|
||||
processedJob.artifacts = job.turtleBuild?.artifacts ?? undefined;
|
||||
} else {
|
||||
processedJob.artifacts = job.turtleJobRun?.artifacts;
|
||||
}
|
||||
delete processedJob.turtleJobRun;
|
||||
return processedJob;
|
||||
});
|
||||
result.jobs = processedJobs;
|
||||
|
||||
result.logURL = getWorkflowRunUrl(
|
||||
result.workflow.app.ownerAccount.name,
|
||||
result.workflow.app.name,
|
||||
@@ -98,7 +145,7 @@ export default class WorkflowView extends EasCommand {
|
||||
{ label: 'Trigger', value: result.trigger ?? 'null' },
|
||||
{
|
||||
label: 'Git Commit Message',
|
||||
value: result.gitCommitMessage?.split('\n')[0] ?? null ?? 'null',
|
||||
value: result.gitCommitMessage?.split('\n')[0] ?? 'null',
|
||||
},
|
||||
{ label: 'Status', value: result.status },
|
||||
{ label: 'Errors', value: result.errors.map(error => error.title).join('\n') },
|
||||
@@ -108,7 +155,7 @@ export default class WorkflowView extends EasCommand {
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
result.jobs.forEach(job => {
|
||||
result.jobs.forEach((job: WorkflowJobResult) => {
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Job ID', value: job.id },
|
||||
@@ -118,10 +165,59 @@ export default class WorkflowView extends EasCommand {
|
||||
{ label: ' Type', value: job.type },
|
||||
{ label: ' Created At', value: job.createdAt },
|
||||
{ label: ' Updated At', value: job.updatedAt },
|
||||
{ label: ' Outputs', value: JSON.stringify(job.outputs, null, 2) },
|
||||
{ label: ' Errors', value: job.errors.map(error => error.title).join('\n') },
|
||||
])
|
||||
);
|
||||
if (job.errors.length > 0) {
|
||||
Log.gray(chalk.dim(' Errors:'));
|
||||
job.errors.forEach(error => {
|
||||
Log.log(formatFields([{ label: ` ${error.title}`, value: `${error.message}` }]));
|
||||
});
|
||||
}
|
||||
if (job.outputs) {
|
||||
const outputs = processedOutputs(job);
|
||||
if (outputs.length > 0) {
|
||||
Log.gray(chalk.dim(' Outputs:'));
|
||||
Log.log(formatFields(outputs));
|
||||
}
|
||||
}
|
||||
if (job.type === WorkflowJobType.Build) {
|
||||
if (job.turtleBuild?.artifacts) {
|
||||
Log.gray(chalk.dim(' Artifacts:'));
|
||||
Log.log(
|
||||
formatFields(
|
||||
formatGraphQLBuildArtifacts(job.turtleBuild).map(item => {
|
||||
item.label = ` ${item.label}`;
|
||||
return item;
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const jobArtifacts = job.artifacts as ReducedWorkflowArtifact[];
|
||||
if (jobArtifacts?.length) {
|
||||
Log.gray(chalk.dim(' Artifacts:'));
|
||||
jobArtifacts.forEach(artifact => {
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: ' ID', value: artifact.id },
|
||||
{ label: ' Name', value: artifact.name },
|
||||
{ label: ' Content Type', value: artifact?.contentType ?? 'null' },
|
||||
{
|
||||
label: ' File Size Bytes',
|
||||
value: artifact?.fileSizeBytes ? `${artifact.fileSizeBytes}` : 'null',
|
||||
},
|
||||
{ label: ' Filename', value: artifact.filename },
|
||||
{
|
||||
label: ' Download URL',
|
||||
value: artifact?.downloadUrl ? link(artifact.downloadUrl) : 'null',
|
||||
},
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +44,21 @@ export const jester2 = {
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
export const robot: Actor = {
|
||||
__typename: 'Robot',
|
||||
id: 'robot-id',
|
||||
firstName: 'Test Robot',
|
||||
accounts: [
|
||||
{
|
||||
id: 'robot-account-id',
|
||||
name: 'robot-account',
|
||||
users: [],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
};
|
||||
|
||||
assert(jester.__typename === 'User');
|
||||
export const testUsername = jester.username;
|
||||
export const testSlug = 'testApp';
|
||||
|
||||
+44
-7
@@ -15,6 +15,7 @@ function createMockBundleId(id: string = 'XXX', capabilities: BundleIdCapability
|
||||
id,
|
||||
} as any;
|
||||
}
|
||||
const ctx = Object.freeze({ providerId: 123195, teamId: 'MyteamId' });
|
||||
|
||||
describe(assertValidOptions, () => {
|
||||
it(`adds a reason for asserting capability identifiers`, () => {
|
||||
@@ -29,7 +30,6 @@ describe(assertValidOptions, () => {
|
||||
|
||||
describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
it(`does not disable associated domains when MDM managed domains is active`, async () => {
|
||||
const ctx = { providerId: 123195, teamId: 'MyteamId' };
|
||||
// https://forums.expo.dev/t/eas-build-failed-on-ios-associated-domains-capability/61662/5
|
||||
const capabilities = [
|
||||
{
|
||||
@@ -67,8 +67,46 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips syncing capabilities with settings if they do not contain the expected `enabled` field', async () => {
|
||||
const capabilityJson = {
|
||||
id: 'U78L9459DG_APPLE_ID_AUTH',
|
||||
attributes: {
|
||||
settings: [
|
||||
{
|
||||
key: 'APPLE_ID_AUTH_APP_CONSENT',
|
||||
options: [
|
||||
{
|
||||
key: 'PRIMARY_APP_CONSENT',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
capabilityType: 'APPLE_ID_AUTH',
|
||||
},
|
||||
};
|
||||
|
||||
const capability = new BundleIdCapability(
|
||||
ctx,
|
||||
capabilityJson.id,
|
||||
capabilityJson.attributes as any
|
||||
);
|
||||
const bundleId = createMockBundleId('U78L9459DG', [capability]);
|
||||
const result = await syncCapabilitiesForEntitlementsAsync(
|
||||
bundleId,
|
||||
{
|
||||
'com.apple.developer.applesignin': ['Default'],
|
||||
},
|
||||
noBroadcastNotificationOption
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
});
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('capabilities with settings', () => {
|
||||
const ctx = { providerId: 123195, teamId: 'MyteamId' };
|
||||
const capabilities = [
|
||||
{
|
||||
id: 'U78L9459DG_APPLE_ID_AUTH',
|
||||
@@ -266,7 +304,6 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
});
|
||||
|
||||
describe('given a bundleId with a capability that is enabled', () => {
|
||||
const ctx = { providerId: 123195, teamId: 'MyteamId' };
|
||||
const remote = {
|
||||
id: 'UFJ54VZ75A_ACCESS_WIFI_INFORMATION',
|
||||
attributes: {
|
||||
@@ -380,7 +417,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
noBroadcastNotificationOption
|
||||
);
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toHaveBeenCalledWith([
|
||||
{ capabilityType: 'HEALTHKIT', option: 'ON' },
|
||||
{ capabilityType: 'APPLE_PAY', option: 'ON' },
|
||||
{ capabilityType: 'ICLOUD', option: 'ON' },
|
||||
@@ -696,7 +733,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
noBroadcastNotificationOption
|
||||
);
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toBeCalledWith([
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).toHaveBeenCalledWith([
|
||||
{ capabilityType: 'HEALTHKIT', option: 'ON' },
|
||||
]);
|
||||
|
||||
@@ -786,7 +823,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
}
|
||||
);
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
|
||||
|
||||
expect(results.disabled).toStrictEqual([]);
|
||||
expect(results.enabled).toStrictEqual([]);
|
||||
@@ -807,7 +844,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
noBroadcastNotificationOption
|
||||
);
|
||||
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toBeCalled();
|
||||
expect(bundleId.updateBundleIdCapabilityAsync).not.toHaveBeenCalled();
|
||||
|
||||
expect(results.disabled).toStrictEqual([]);
|
||||
expect(results.enabled).toStrictEqual([]);
|
||||
|
||||
@@ -129,7 +129,7 @@ function getCapabilitiesToEnable(
|
||||
const { op } = operation;
|
||||
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Will ${op} remote capability: ${key} (${staticCapabilityInfo.name}.`);
|
||||
Log.log(`Will ${op} remote capability: ${key} (${staticCapabilityInfo.name}).`);
|
||||
}
|
||||
if (op === 'enable') {
|
||||
enabledCapabilityNames.push(staticCapabilityInfo.name);
|
||||
|
||||
@@ -12,6 +12,8 @@ import { JSONObject, JSONValue } from '@expo/json-file';
|
||||
import { invariant } from 'graphql/jsutils/invariant';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import Log from '../../../log';
|
||||
|
||||
const validateBooleanOptions = (options: any): boolean => {
|
||||
return typeof options === 'boolean';
|
||||
};
|
||||
@@ -86,21 +88,32 @@ const capabilityWithSettingsSyncOperation: GetSyncOperation = ({
|
||||
if (!existingRemote) {
|
||||
return enableOp;
|
||||
}
|
||||
invariant(
|
||||
'enabled' in existingRemote.attributes,
|
||||
`Expected "enabled" attribute in ${existingRemote.id}`
|
||||
);
|
||||
const existingEnabled = existingRemote.attributes.enabled === true;
|
||||
const newOption = entitlementValue ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF;
|
||||
const { attributes, id } = existingRemote;
|
||||
if ('enabled' in attributes) {
|
||||
// the `enabled` field should be available as per https://developer.apple.com/documentation/appstoreconnectapi/capabilitysetting
|
||||
const existingEnabled = attributes.enabled === true;
|
||||
|
||||
// If both are enabled and the existing one has settings, skip the update
|
||||
if (existingEnabled && entitlementValue && existingRemote.attributes.settings) {
|
||||
// If both are enabled and the existing one has settings, skip the update
|
||||
if (existingEnabled && entitlementValue && attributes.settings) {
|
||||
return skipOp;
|
||||
}
|
||||
|
||||
const newOption = entitlementValue ? CapabilityTypeOption.ON : CapabilityTypeOption.OFF;
|
||||
// If the states don't match, we need to update
|
||||
const newEnabled = newOption === CapabilityTypeOption.ON;
|
||||
return existingEnabled === newEnabled ? skipOp : { op: 'enable', option: newOption };
|
||||
} else {
|
||||
if (Log.isDebug) {
|
||||
Log.log(
|
||||
`Expected the "enabled" attribute in ${id} but it was not present (attributes: ${JSON.stringify(
|
||||
attributes,
|
||||
null,
|
||||
2
|
||||
)}). Will skip syncing this capability.`
|
||||
);
|
||||
}
|
||||
return skipOp;
|
||||
}
|
||||
|
||||
// If the states don't match, we need to update
|
||||
const newEnabled = newOption === CapabilityTypeOption.ON;
|
||||
return existingEnabled === newEnabled ? skipOp : { op: 'enable', option: newOption };
|
||||
};
|
||||
|
||||
export type CapabilityClassifier = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
AccountFragment,
|
||||
@@ -33,3 +34,42 @@ export type IosCredentials = Record<string, TargetCredentials>;
|
||||
|
||||
export type IosAppBuildCredentialsMap = Record<string, IosAppBuildCredentialsFragment>;
|
||||
export type IosAppCredentialsMap = Record<string, CommonIosAppCredentialsFragment | null>;
|
||||
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
|
||||
// which is not what we want. See:
|
||||
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
|
||||
|
||||
export const booleanLike = z.union([
|
||||
z.boolean(),
|
||||
z.codec(z.number(), z.boolean(), {
|
||||
decode: n => !!n,
|
||||
encode: b => (b ? 1 : 0),
|
||||
}),
|
||||
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
|
||||
]);
|
||||
|
||||
export const stringLike = z.codec(
|
||||
z.union([
|
||||
// We're going to coerce numbers and strings into strings.
|
||||
z.number(),
|
||||
z.string(),
|
||||
// We do not allow other primitives, like:
|
||||
// - bigints, symbols - because YAML does not support them,
|
||||
// - booleans - because YAML accepts `True` and `true` as boolean input
|
||||
// and parses both as JS `true` -- if we stringified that,
|
||||
// we would lose the capital "T" which may not be what the user expects,
|
||||
// - nulls - user should do `"null"` or not pass the property at all.
|
||||
]),
|
||||
z.string(),
|
||||
{
|
||||
decode: value => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
|
||||
},
|
||||
encode: value => value,
|
||||
}
|
||||
);
|
||||
+217
-90
@@ -21,8 +21,10 @@ export type Scalars = {
|
||||
Float: { input: number; output: number; }
|
||||
DateTime: { input: any; output: any; }
|
||||
DevDomainName: { input: any; output: any; }
|
||||
EnvironmentVariableEnvironment: { input: any; output: any; }
|
||||
JSON: { input: any; output: any; }
|
||||
JSONObject: { input: any; output: any; }
|
||||
SubmissionAndroidTrack: { input: any; output: any; }
|
||||
WorkerDeploymentIdentifier: { input: any; output: any; }
|
||||
WorkerDeploymentRequestID: { input: any; output: any; }
|
||||
};
|
||||
@@ -122,6 +124,7 @@ export type Account = {
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
/** Environment secrets for an account */
|
||||
environmentSecrets: Array<EnvironmentSecret>;
|
||||
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
/** Environment variables for an account */
|
||||
environmentVariables: Array<EnvironmentVariable>;
|
||||
/** Environment variables for an account with decrypted secret values */
|
||||
@@ -185,6 +188,8 @@ export type Account = {
|
||||
userInvitations: Array<UserInvitation>;
|
||||
/** Actors associated with this account and permissions they hold */
|
||||
users: Array<UserPermission>;
|
||||
/** Notification preferences of the viewer for this account */
|
||||
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
|
||||
/** Permission info for the viewer on this account */
|
||||
viewerUserPermission: UserPermission;
|
||||
/** @deprecated Build packs are no longer supported */
|
||||
@@ -386,7 +391,7 @@ export type AccountEnvironmentSecretsArgs = {
|
||||
* data and settings. Actors may own and be members of accounts.
|
||||
*/
|
||||
export type AccountEnvironmentVariablesArgs = {
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
@@ -396,7 +401,7 @@ export type AccountEnvironmentVariablesArgs = {
|
||||
* data and settings. Actors may own and be members of accounts.
|
||||
*/
|
||||
export type AccountEnvironmentVariablesIncludingSensitiveArgs = {
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
@@ -644,6 +649,22 @@ export type AccountMutationSetPushSecurityEnabledArgs = {
|
||||
pushSecurityEnabled: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
/** Account-level notification preference */
|
||||
export type AccountNotificationPreference = NotificationPreference & {
|
||||
__typename?: 'AccountNotificationPreference';
|
||||
account: Account;
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AccountNotificationPreferenceInput = {
|
||||
accountId: Scalars['ID']['input'];
|
||||
enabled: Scalars['Boolean']['input'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AccountNotificationSubscriptionInput = {
|
||||
accountId: Scalars['ID']['input'];
|
||||
event: NotificationEvent;
|
||||
@@ -1125,7 +1146,7 @@ export type AndroidJobInput = {
|
||||
cache?: InputMaybe<BuildCacheInput>;
|
||||
customBuildConfig?: InputMaybe<CustomBuildConfigInput>;
|
||||
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
gradleCommand?: InputMaybe<Scalars['String']['input']>;
|
||||
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
|
||||
@@ -1235,12 +1256,11 @@ export type AndroidSubmissionConfig = {
|
||||
archiveType?: Maybe<SubmissionAndroidArchiveType>;
|
||||
releaseStatus?: Maybe<SubmissionAndroidReleaseStatus>;
|
||||
rollout?: Maybe<Scalars['Float']['output']>;
|
||||
track: SubmissionAndroidTrack;
|
||||
track: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AndroidSubmissionConfigInput = {
|
||||
applicationIdentifier?: InputMaybe<Scalars['String']['input']>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
changelog?: InputMaybe<Scalars['String']['input']>;
|
||||
changesNotSentForReview?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
googleServiceAccountKeyId?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -1248,7 +1268,7 @@ export type AndroidSubmissionConfigInput = {
|
||||
isVerboseFastlaneEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
releaseStatus?: InputMaybe<SubmissionAndroidReleaseStatus>;
|
||||
rollout?: InputMaybe<Scalars['Float']['input']>;
|
||||
track: SubmissionAndroidTrack;
|
||||
track: Scalars['SubmissionAndroidTrack']['input'];
|
||||
};
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
@@ -1284,6 +1304,7 @@ export type App = Project & {
|
||||
devDomainName?: Maybe<AppDevDomainName>;
|
||||
/** Environment secrets for an app */
|
||||
environmentSecrets: Array<EnvironmentSecret>;
|
||||
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
/** Environment variables for an app */
|
||||
environmentVariables: Array<EnvironmentVariable>;
|
||||
/** Environment variables for an app with decrypted secret values */
|
||||
@@ -1413,6 +1434,8 @@ export type App = Project & {
|
||||
username: Scalars['String']['output'];
|
||||
/** @deprecated No longer supported */
|
||||
users?: Maybe<Array<Maybe<User>>>;
|
||||
/** Notification preferences of the viewer for this app */
|
||||
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
|
||||
/** Webhooks for an app */
|
||||
webhooks: Array<Webhook>;
|
||||
workerCustomDomain?: Maybe<WorkerCustomDomain>;
|
||||
@@ -1511,14 +1534,14 @@ export type AppEnvironmentSecretsArgs = {
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppEnvironmentVariablesArgs = {
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppEnvironmentVariablesIncludingSensitiveArgs = {
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
@@ -1915,6 +1938,22 @@ export type AppMutationSetResourceClassExperimentArgs = {
|
||||
resourceClassExperiment?: InputMaybe<ResourceClassExperiment>;
|
||||
};
|
||||
|
||||
/** App-level notification preference */
|
||||
export type AppNotificationPreference = NotificationPreference & {
|
||||
__typename?: 'AppNotificationPreference';
|
||||
app: App;
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AppNotificationPreferenceInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
enabled: Scalars['Boolean']['input'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AppNotificationSubscriptionInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
event: NotificationEvent;
|
||||
@@ -2944,7 +2983,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
reactNativeVersion?: Maybe<Scalars['String']['output']>;
|
||||
releaseChannel?: Maybe<Scalars['String']['output']>;
|
||||
requiredPackageManager?: Maybe<Scalars['String']['output']>;
|
||||
resolvedEnvironment?: Maybe<EnvironmentVariableEnvironment>;
|
||||
resolvedEnvironment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
/**
|
||||
* The builder resource class requested by the developer
|
||||
* @deprecated Use resourceClassDisplayName instead
|
||||
@@ -2964,7 +3003,9 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
submissions: Array<Submission>;
|
||||
updateChannel?: Maybe<UpdateChannel>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
waiverType?: Maybe<EasBuildWaiverType>;
|
||||
workerStartedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
workflowJob?: Maybe<WorkflowJob>;
|
||||
};
|
||||
|
||||
|
||||
@@ -3564,7 +3605,6 @@ export type CreateAndConfigureRepositoryInput = {
|
||||
export type CreateAndroidSubmissionInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
config: AndroidSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
|
||||
};
|
||||
@@ -3582,7 +3622,7 @@ export type CreateEnvironmentSecretInput = {
|
||||
};
|
||||
|
||||
export type CreateEnvironmentVariableInput = {
|
||||
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
|
||||
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
|
||||
fileName?: InputMaybe<Scalars['String']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
@@ -3605,7 +3645,7 @@ export type CreateGitHubBuildTriggerInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
autoSubmit: Scalars['Boolean']['input'];
|
||||
buildProfile: Scalars['String']['input'];
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
executionBehavior: GitHubBuildTriggerExecutionBehavior;
|
||||
isActive: Scalars['Boolean']['input'];
|
||||
platform: AppPlatform;
|
||||
@@ -3642,7 +3682,6 @@ export type CreateGitHubRepositorySettingsInput = {
|
||||
export type CreateIosSubmissionInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
archiveSource?: InputMaybe<SubmissionArchiveSourceInput>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
config: IosSubmissionConfigInput;
|
||||
submittedBuildId?: InputMaybe<Scalars['ID']['input']>;
|
||||
};
|
||||
@@ -3654,9 +3693,8 @@ export type CreateSentryProjectInput = {
|
||||
};
|
||||
|
||||
export type CreateSharedEnvironmentVariableInput = {
|
||||
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
|
||||
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
|
||||
fileName?: InputMaybe<Scalars['String']['input']>;
|
||||
isGlobal?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
overwrite?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
type?: InputMaybe<EnvironmentSecretType>;
|
||||
@@ -4231,11 +4269,11 @@ export type EnvironmentVariable = {
|
||||
/** @deprecated Environment variables are automatically linked to all apps */
|
||||
apps: Array<App>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
|
||||
environments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
|
||||
fileName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
isGlobal?: Maybe<Scalars['Boolean']['output']>;
|
||||
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
|
||||
linkedEnvironments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
|
||||
name: Scalars['String']['output'];
|
||||
scope: EnvironmentVariableScope;
|
||||
type: EnvironmentSecretType;
|
||||
@@ -4255,12 +4293,6 @@ export type EnvironmentVariableValueArgs = {
|
||||
includeFileContent?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
export enum EnvironmentVariableEnvironment {
|
||||
Development = 'DEVELOPMENT',
|
||||
Preview = 'PREVIEW',
|
||||
Production = 'PRODUCTION'
|
||||
}
|
||||
|
||||
export type EnvironmentVariableMutation = {
|
||||
__typename?: 'EnvironmentVariableMutation';
|
||||
/** Create bulk env variables for an Account */
|
||||
@@ -4340,11 +4372,11 @@ export type EnvironmentVariableWithSecret = {
|
||||
__typename?: 'EnvironmentVariableWithSecret';
|
||||
apps: Array<App>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
environments?: Maybe<Array<EnvironmentVariableEnvironment>>;
|
||||
environments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
|
||||
fileName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
isGlobal: Scalars['Boolean']['output'];
|
||||
linkedEnvironments?: Maybe<Array<EnvironmentVariableEnvironment>>;
|
||||
linkedEnvironments?: Maybe<Array<Scalars['EnvironmentVariableEnvironment']['output']>>;
|
||||
name: Scalars['String']['output'];
|
||||
scope: EnvironmentVariableScope;
|
||||
sensitive: Scalars['Boolean']['output'];
|
||||
@@ -4384,11 +4416,24 @@ export type EstimatedUsage = {
|
||||
id: Scalars['ID']['output'];
|
||||
limit: Scalars['Float']['output'];
|
||||
metricType: UsageMetricType;
|
||||
platformBreakdown?: Maybe<EstimatedUsagePlatformBreakdown>;
|
||||
service: EasService;
|
||||
serviceMetric: EasServiceMetric;
|
||||
value: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export type EstimatedUsagePlatformBreakdown = {
|
||||
__typename?: 'EstimatedUsagePlatformBreakdown';
|
||||
android: EstimatedUsagePlatformDetail;
|
||||
ios: EstimatedUsagePlatformDetail;
|
||||
};
|
||||
|
||||
export type EstimatedUsagePlatformDetail = {
|
||||
__typename?: 'EstimatedUsagePlatformDetail';
|
||||
limit: Scalars['Float']['output'];
|
||||
value: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export enum Experiment {
|
||||
Orbit = 'ORBIT'
|
||||
}
|
||||
@@ -4649,7 +4694,7 @@ export type GitHubBuildInput = {
|
||||
autoSubmit?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
baseDirectory?: InputMaybe<Scalars['String']['input']>;
|
||||
buildProfile: Scalars['String']['input'];
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
gitRef: Scalars['String']['input'];
|
||||
platform: AppPlatform;
|
||||
submitProfile?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -4661,7 +4706,7 @@ export type GitHubBuildTrigger = {
|
||||
autoSubmit: Scalars['Boolean']['output'];
|
||||
buildProfile: Scalars['String']['output'];
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
environment?: Maybe<EnvironmentVariableEnvironment>;
|
||||
environment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
executionBehavior: GitHubBuildTriggerExecutionBehavior;
|
||||
id: Scalars['ID']['output'];
|
||||
isActive: Scalars['Boolean']['output'];
|
||||
@@ -5240,7 +5285,7 @@ export type IosJobInput = {
|
||||
developmentClient?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
/** @deprecated */
|
||||
distribution?: InputMaybe<DistributionType>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
experimental?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
loggerLevel?: InputMaybe<WorkerLoggerLevel>;
|
||||
mode?: InputMaybe<BuildMode>;
|
||||
@@ -5322,7 +5367,6 @@ export type IosSubmissionConfig = {
|
||||
export type IosSubmissionConfigInput = {
|
||||
appleAppSpecificPassword?: InputMaybe<Scalars['String']['input']>;
|
||||
appleIdUsername?: InputMaybe<Scalars['String']['input']>;
|
||||
archiveUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
ascApiKey?: InputMaybe<AscApiKeyInput>;
|
||||
ascApiKeyId?: InputMaybe<Scalars['String']['input']>;
|
||||
ascAppIdentifier: Scalars['String']['input'];
|
||||
@@ -5605,7 +5649,10 @@ export type MeMutation = {
|
||||
scheduleCurrentUserDeletion: BackgroundJobReceipt;
|
||||
/** Schedule deletion of a SSO user. Actor must be an owner on the SSO user's SSO account. */
|
||||
scheduleSSOUserDeletionAsSSOAccountOwner: BackgroundJobReceipt;
|
||||
/** Send SMS OTP to a second factor device for use during device setup or during change confirmation */
|
||||
/**
|
||||
* Send SMS OTP to a second factor device for use during device setup or during change confirmation
|
||||
* @deprecated This method is no longer supported.
|
||||
*/
|
||||
sendSMSOTPToSecondFactorDevice: SecondFactorBooleanResult;
|
||||
/**
|
||||
* Sets user preferences. This is a key-value store for user-specific settings. Provided values are
|
||||
@@ -5749,6 +5796,32 @@ export enum NotificationEvent {
|
||||
|
||||
export type NotificationMetadata = BuildLimitThresholdExceededMetadata | BuildPlanCreditThresholdExceededMetadata | TestNotificationMetadata;
|
||||
|
||||
/** Base interface for notification preferences */
|
||||
export type NotificationPreference = {
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
/** Union type representing either account-level or app-level notification preferences */
|
||||
export type NotificationPreferenceItem = AccountNotificationPreference | AppNotificationPreference;
|
||||
|
||||
export type NotificationPreferenceMutation = {
|
||||
__typename?: 'NotificationPreferenceMutation';
|
||||
setAccountNotificationPreference: NotificationPreferenceItem;
|
||||
setAppNotificationPreference: NotificationPreferenceItem;
|
||||
};
|
||||
|
||||
|
||||
export type NotificationPreferenceMutationSetAccountNotificationPreferenceArgs = {
|
||||
input: AccountNotificationPreferenceInput;
|
||||
};
|
||||
|
||||
|
||||
export type NotificationPreferenceMutationSetAppNotificationPreferenceArgs = {
|
||||
input: AppNotificationPreferenceInput;
|
||||
};
|
||||
|
||||
export type NotificationSubscription = {
|
||||
__typename?: 'NotificationSubscription';
|
||||
account?: Maybe<Account>;
|
||||
@@ -5967,7 +6040,7 @@ export type PublishUpdateGroupInput = {
|
||||
assetMapGroup?: InputMaybe<AssetMapGroup>;
|
||||
awaitingCodeSigningInfo?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
branchId: Scalars['String']['input'];
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
excludedAssets?: InputMaybe<Array<PartialManifestAsset>>;
|
||||
fingerprintInfoGroup?: InputMaybe<FingerprintInfoGroup>;
|
||||
gitCommitHash?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -6224,7 +6297,12 @@ export type RootMutation = {
|
||||
logRocketProject: LogRocketProjectMutation;
|
||||
/** Mutations that modify the currently authenticated User */
|
||||
me: MeMutation;
|
||||
/** Mutations that modify a NotificationSubscription */
|
||||
/** Notification preference management */
|
||||
notificationPreference: NotificationPreferenceMutation;
|
||||
/**
|
||||
* Mutations that modify a NotificationSubscription
|
||||
* @deprecated Use notificationPreference mutation instead
|
||||
*/
|
||||
notificationSubscription: NotificationSubscriptionMutation;
|
||||
/** Mutations that create, update, and delete Robots */
|
||||
robot: RobotMutation;
|
||||
@@ -6565,6 +6643,7 @@ export type SsoUser = Actor & UserActor & {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
pinnedApps: Array<App>;
|
||||
pinnedDashboardViews: Array<PinnedDashboardView>;
|
||||
@@ -6920,6 +6999,7 @@ export type Submission = ActivityTimelineProjectActivity & {
|
||||
id: Scalars['ID']['output'];
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
iosConfig?: Maybe<IosSubmissionConfig>;
|
||||
jobRun?: Maybe<JobRun>;
|
||||
logFiles: Array<Scalars['String']['output']>;
|
||||
/** @deprecated Use logFiles instead */
|
||||
logsUrl?: Maybe<Scalars['String']['output']>;
|
||||
@@ -6931,6 +7011,7 @@ export type Submission = ActivityTimelineProjectActivity & {
|
||||
status: SubmissionStatus;
|
||||
submittedBuild?: Maybe<Build>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
workflowJob?: Maybe<WorkflowJob>;
|
||||
};
|
||||
|
||||
export enum SubmissionAndroidArchiveType {
|
||||
@@ -6945,13 +7026,6 @@ export enum SubmissionAndroidReleaseStatus {
|
||||
InProgress = 'IN_PROGRESS'
|
||||
}
|
||||
|
||||
export enum SubmissionAndroidTrack {
|
||||
Alpha = 'ALPHA',
|
||||
Beta = 'BETA',
|
||||
Internal = 'INTERNAL',
|
||||
Production = 'PRODUCTION'
|
||||
}
|
||||
|
||||
export type SubmissionArchiveSourceInput = {
|
||||
/** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE, GCS_BUILD_APPLICATION_ARCHIVE_ORCHESTRATOR or GCS_SUBMIT_ARCHIVE */
|
||||
bucketKey?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -7122,7 +7196,7 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
codeSigningInfo?: Maybe<CodeSigningInfo>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
deployments: DeploymentResult;
|
||||
environment?: Maybe<EnvironmentVariableEnvironment>;
|
||||
environment?: Maybe<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
expoGoSDKVersion?: Maybe<Scalars['String']['output']>;
|
||||
fingerprint?: Maybe<Fingerprint>;
|
||||
gitCommitHash?: Maybe<Scalars['String']['output']>;
|
||||
@@ -7144,6 +7218,7 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
/** @deprecated Use 'runtime' field . */
|
||||
runtimeVersion: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
workflowJob?: Maybe<WorkflowJob>;
|
||||
};
|
||||
|
||||
|
||||
@@ -7354,10 +7429,9 @@ export type UpdateDeploymentsConnection = {
|
||||
};
|
||||
|
||||
export type UpdateEnvironmentVariableInput = {
|
||||
environments?: InputMaybe<Array<EnvironmentVariableEnvironment>>;
|
||||
environments?: InputMaybe<Array<Scalars['EnvironmentVariableEnvironment']['input']>>;
|
||||
fileName?: InputMaybe<Scalars['String']['input']>;
|
||||
id: Scalars['ID']['input'];
|
||||
isGlobal?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
type?: InputMaybe<EnvironmentSecretType>;
|
||||
value?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -7373,7 +7447,7 @@ export type UpdateFilterInput = {
|
||||
export type UpdateGitHubBuildTriggerInput = {
|
||||
autoSubmit: Scalars['Boolean']['input'];
|
||||
buildProfile: Scalars['String']['input'];
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
executionBehavior: GitHubBuildTriggerExecutionBehavior;
|
||||
isActive: Scalars['Boolean']['input'];
|
||||
platform: AppPlatform;
|
||||
@@ -7617,6 +7691,7 @@ export type User = Actor & UserActor & {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
/** Pending UserInvitations for this user. Only resolves for the viewer. */
|
||||
pendingUserInvitations: Array<UserInvitation>;
|
||||
@@ -7729,6 +7804,7 @@ export type UserActor = {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
pinnedApps: Array<App>;
|
||||
preferences: UserPreferences;
|
||||
@@ -8882,6 +8958,7 @@ export enum WorkflowJobType {
|
||||
Doc = 'DOC',
|
||||
Fingerprint = 'FINGERPRINT',
|
||||
GetBuild = 'GET_BUILD',
|
||||
GithubComment = 'GITHUB_COMMENT',
|
||||
MaestroCloud = 'MAESTRO_CLOUD',
|
||||
MaestroTest = 'MAESTRO_TEST',
|
||||
Repack = 'REPACK',
|
||||
@@ -8946,10 +9023,18 @@ export type WorkflowRevisionInput = {
|
||||
|
||||
export type WorkflowRevisionMutation = {
|
||||
__typename?: 'WorkflowRevisionMutation';
|
||||
getOrCreateWorkflowRevisionFromGitRef: WorkflowRevision;
|
||||
validateWorkflowYamlConfig: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkflowRevisionMutationGetOrCreateWorkflowRevisionFromGitRefArgs = {
|
||||
appId: Scalars['ID']['input'];
|
||||
fileName: Scalars['String']['input'];
|
||||
gitRef: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkflowRevisionMutationValidateWorkflowYamlConfigArgs = {
|
||||
appId: Scalars['ID']['input'];
|
||||
yamlConfig: Scalars['String']['input'];
|
||||
@@ -8978,6 +9063,7 @@ export type WorkflowRun = ActivityTimelineProjectActivity & {
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
durationSeconds?: Maybe<Scalars['Int']['output']>;
|
||||
errors: Array<WorkflowRunError>;
|
||||
finalizedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
gitCommitHash?: Maybe<Scalars['String']['output']>;
|
||||
gitCommitMessage?: Maybe<Scalars['String']['output']>;
|
||||
githubRepository?: Maybe<GitHubRepository>;
|
||||
@@ -9028,6 +9114,7 @@ export type WorkflowRunMutation = {
|
||||
__typename?: 'WorkflowRunMutation';
|
||||
cancelWorkflowRun: WorkflowRun;
|
||||
createWorkflowRun: WorkflowRun;
|
||||
createWorkflowRunFromGitRef: WorkflowRun;
|
||||
retryWorkflowRun: WorkflowRun;
|
||||
};
|
||||
|
||||
@@ -9044,6 +9131,13 @@ export type WorkflowRunMutationCreateWorkflowRunArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type WorkflowRunMutationCreateWorkflowRunFromGitRefArgs = {
|
||||
gitRef: Scalars['String']['input'];
|
||||
inputs?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
workflowRevisionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkflowRunMutationRetryWorkflowRunArgs = {
|
||||
fromFailedJobs?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
workflowRunId: Scalars['ID']['input'];
|
||||
@@ -9634,7 +9728,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type CreateIosBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -9644,7 +9738,7 @@ export type CreateIosBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type UpdateBuildMetadataMutationVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
@@ -9652,7 +9746,7 @@ export type UpdateBuildMetadataMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type RetryIosBuildMutationVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
@@ -9660,7 +9754,7 @@ export type RetryIosBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
@@ -9691,7 +9785,7 @@ export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
|
||||
export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
|
||||
input: CreateEnvironmentVariableInput;
|
||||
@@ -9699,14 +9793,14 @@ export type CreateEnvironmentVariableForAppMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
|
||||
export type UpdateEnvironmentVariableMutationVariables = Exact<{
|
||||
input: UpdateEnvironmentVariableInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
export type UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } };
|
||||
|
||||
export type DeleteEnvironmentVariableMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
@@ -9744,7 +9838,7 @@ export type CreateLocalBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
|
||||
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
|
||||
|
||||
export type GetSignedUploadMutationVariables = Exact<{
|
||||
contentTypes: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
||||
@@ -9758,7 +9852,7 @@ export type UpdatePublishMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> } };
|
||||
export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> } };
|
||||
|
||||
export type SetCodeSigningInfoMutationVariables = Exact<{
|
||||
updateId: Scalars['ID']['input'];
|
||||
@@ -9774,7 +9868,7 @@ export type SetRolloutPercentageMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type SetRolloutPercentageMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setRolloutPercentage: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
|
||||
export type SetRolloutPercentageMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setRolloutPercentage: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
|
||||
|
||||
export type CreateAndroidSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -9784,7 +9878,7 @@ export type CreateAndroidSubmissionMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAndroidSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createAndroidSubmission: { __typename?: 'CreateSubmissionResult', submission: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } } };
|
||||
export type CreateAndroidSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createAndroidSubmission: { __typename?: 'CreateSubmissionResult', submission: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } } };
|
||||
|
||||
export type CreateIosSubmissionMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -9794,7 +9888,7 @@ export type CreateIosSubmissionMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIosSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createIosSubmission: { __typename?: 'CreateSubmissionResult', submission: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } } };
|
||||
export type CreateIosSubmissionMutation = { __typename?: 'RootMutation', submission: { __typename?: 'SubmissionMutation', createIosSubmission: { __typename?: 'CreateSubmissionResult', submission: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } } };
|
||||
|
||||
export type CreateUploadSessionMutationVariables = Exact<{
|
||||
type: UploadSessionType;
|
||||
@@ -9842,6 +9936,15 @@ export type DeleteWebhookMutationVariables = Exact<{
|
||||
|
||||
export type DeleteWebhookMutation = { __typename?: 'RootMutation', webhook: { __typename?: 'WebhookMutation', deleteWebhook: { __typename?: 'DeleteWebhookResult', id: string } } };
|
||||
|
||||
export type GetOrCreateWorkflowRevisionFromGitRefMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
fileName: Scalars['String']['input'];
|
||||
gitRef: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetOrCreateWorkflowRevisionFromGitRefMutation = { __typename?: 'RootMutation', workflowRevision: { __typename?: 'WorkflowRevisionMutation', getOrCreateWorkflowRevisionFromGitRef: { __typename?: 'WorkflowRevision', id: string, yamlConfig: string, blobSha: string, commitSha?: string | null, createdAt: any, workflow: { __typename?: 'Workflow', id: string } } } };
|
||||
|
||||
export type ValidateWorkflowYamlConfigMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
yamlConfig: Scalars['String']['input'];
|
||||
@@ -9859,6 +9962,15 @@ export type CreateWorkflowRunMutationVariables = Exact<{
|
||||
|
||||
export type CreateWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', createWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
|
||||
|
||||
export type CreateWorkflowRunFromGitRefMutationVariables = Exact<{
|
||||
workflowRevisionId: Scalars['ID']['input'];
|
||||
gitRef: Scalars['String']['input'];
|
||||
inputs?: InputMaybe<Scalars['JSONObject']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateWorkflowRunFromGitRefMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', createWorkflowRunFromGitRef: { __typename?: 'WorkflowRun', id: string } } };
|
||||
|
||||
export type CancelWorkflowRunMutationVariables = Exact<{
|
||||
workflowRunId: Scalars['ID']['input'];
|
||||
}>;
|
||||
@@ -9866,6 +9978,14 @@ export type CancelWorkflowRunMutationVariables = Exact<{
|
||||
|
||||
export type CancelWorkflowRunMutation = { __typename?: 'RootMutation', workflowRun: { __typename?: 'WorkflowRunMutation', cancelWorkflowRun: { __typename?: 'WorkflowRun', id: string } } };
|
||||
|
||||
export type AccountUsageForOverageWarningQueryVariables = Exact<{
|
||||
accountId: Scalars['String']['input'];
|
||||
currentDate: Scalars['DateTime']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountUsageForOverageWarningQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byId: { __typename?: 'Account', id: string, name: string, subscription?: { __typename?: 'SubscriptionDetails', id: string, name?: string | null } | null, usageMetrics: { __typename?: 'AccountUsageMetrics', EAS_BUILD: { __typename?: 'UsageMetricTotal', id: string, planMetrics: Array<{ __typename?: 'EstimatedUsage', id: string, serviceMetric: EasServiceMetric, value: number, limit: number }> } } } } };
|
||||
|
||||
export type AppByIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -9885,7 +10005,7 @@ export type AppByIdWorkflowsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type AppByIdWorkflowsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, workflows: Array<{ __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any }> } } };
|
||||
export type AppByIdWorkflowsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, workflows: Array<{ __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } }> } } };
|
||||
|
||||
export type AppByIdWorkflowRunsFilteredByStatusQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -9944,7 +10064,7 @@ export type BranchesByAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> }> } } };
|
||||
export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> }> } } };
|
||||
|
||||
export type BranchesBasicPaginatedOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -9965,28 +10085,28 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
|
||||
export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
|
||||
|
||||
export type BuildsByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type ViewBuildsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -9996,7 +10116,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
|
||||
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
|
||||
|
||||
export type ViewUpdateChannelBasicInfoOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10013,7 +10133,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
|
||||
export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> } | null } } };
|
||||
|
||||
export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10022,7 +10142,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> }> } } };
|
||||
export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, isPaused: boolean, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> }> }> } } };
|
||||
|
||||
export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10042,45 +10162,52 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
|
||||
export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> }, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> } } };
|
||||
|
||||
export type AppEnvironmentVariableEnvironmentsQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppEnvironmentVariableEnvironmentsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariableEnvironments: Array<any> } } };
|
||||
|
||||
export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
includeFileContent: Scalars['Boolean']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
|
||||
export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
|
||||
|
||||
export type EnvironmentVariablesByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
includeFileContent: Scalars['Boolean']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<EnvironmentVariableEnvironment> | null, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
|
||||
export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<any> | null, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } };
|
||||
|
||||
export type EnvironmentVariablesSharedQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
includeFileContent: Scalars['Boolean']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<EnvironmentVariableEnvironment> | null, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
|
||||
export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array<any> | null, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
|
||||
|
||||
export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
|
||||
environment?: InputMaybe<EnvironmentVariableEnvironment>;
|
||||
environment?: InputMaybe<Scalars['EnvironmentVariableEnvironment']['input']>;
|
||||
includeFileContent: Scalars['Boolean']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
|
||||
export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } };
|
||||
|
||||
export type FingerprintsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10140,7 +10267,7 @@ export type SubmissionsByIdQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type SubmissionsByIdQuery = { __typename?: 'RootQuery', submissions: { __typename?: 'SubmissionQuery', byId: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } };
|
||||
export type SubmissionsByIdQuery = { __typename?: 'RootQuery', submissions: { __typename?: 'SubmissionQuery', byId: { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null } } };
|
||||
|
||||
export type GetAllSubmissionsForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10151,14 +10278,14 @@ export type GetAllSubmissionsForAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }> } } };
|
||||
export type GetAllSubmissionsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }> } } };
|
||||
|
||||
export type ViewUpdatesByGroupQueryVariables = Exact<{
|
||||
groupId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
|
||||
export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
|
||||
|
||||
export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10169,7 +10296,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } | null } } };
|
||||
export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } | null } } };
|
||||
|
||||
export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10179,14 +10306,14 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } } };
|
||||
export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }>> } } };
|
||||
|
||||
export type UpdateByIdQueryVariables = Exact<{
|
||||
updateId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateByIdQuery = { __typename?: 'RootQuery', updates: { __typename?: 'UpdateQuery', byId: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
|
||||
export type UpdateByIdQuery = { __typename?: 'RootQuery', updates: { __typename?: 'UpdateQuery', byId: { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null } } };
|
||||
|
||||
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -10213,7 +10340,7 @@ export type WorkflowJobByIdQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
|
||||
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
|
||||
|
||||
export type WorkflowRunByIdQueryVariables = Exact<{
|
||||
workflowRunId: Scalars['ID']['input'];
|
||||
@@ -10227,7 +10354,7 @@ export type WorkflowRunByIdWithJobsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
|
||||
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
|
||||
|
||||
export type WorkflowRunsForAppIdFileNameAndStatusQueryVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -10245,17 +10372,17 @@ export type AppFragment = { __typename?: 'App', id: string, name: string, fullNa
|
||||
|
||||
export type BackgroundJobReceiptDataFragment = { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any };
|
||||
|
||||
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
|
||||
|
||||
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
|
||||
export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType };
|
||||
|
||||
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<EnvironmentVariableEnvironment> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
|
||||
export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array<any> | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType };
|
||||
|
||||
export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } };
|
||||
|
||||
@@ -10263,11 +10390,11 @@ export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: str
|
||||
|
||||
export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id: string, name: StatuspageServiceName, status: StatuspageServiceStatus, incidents: Array<{ __typename?: 'StatuspageIncident', id: string, status: StatuspageIncidentStatus, name: string, impact: StatuspageIncidentImpact, shortlink: string }> };
|
||||
|
||||
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
|
||||
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
|
||||
|
||||
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null };
|
||||
export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null };
|
||||
|
||||
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: EnvironmentVariableEnvironment | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
|
||||
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, isGitWorkingTreeDirty: boolean, environment?: any | null, rolloutPercentage?: number | null, manifestHostOverride?: string | null, assetHostOverride?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string, group: string } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, source?: { __typename?: 'FingerprintSource', type: FingerprintSourceType, bucketKey: string, isDebugFingerprint?: boolean | null } | null } | null }> };
|
||||
|
||||
export type UpdateBranchBasicInfoFragment = { __typename?: 'UpdateBranch', id: string, name: string };
|
||||
|
||||
@@ -10275,9 +10402,9 @@ export type UpdateChannelBasicInfoFragment = { __typename?: 'UpdateChannel', id:
|
||||
|
||||
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
|
||||
|
||||
export type WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any };
|
||||
export type WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } };
|
||||
|
||||
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
|
||||
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
|
||||
|
||||
export type WorkflowRunFragment = { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }>, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } };
|
||||
|
||||
|
||||
@@ -7,37 +7,19 @@ import {
|
||||
CreateBulkEnvironmentVariablesForAppMutation,
|
||||
CreateEnvironmentVariableForAccountMutation,
|
||||
CreateEnvironmentVariableForAppMutation,
|
||||
CreateEnvironmentVariableInput,
|
||||
CreateSharedEnvironmentVariableInput,
|
||||
DeleteEnvironmentVariableMutation,
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariableVisibility,
|
||||
UpdateEnvironmentVariableInput,
|
||||
UpdateEnvironmentVariableMutation,
|
||||
} from '../generated';
|
||||
import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
|
||||
|
||||
type CreateVariableArgs = {
|
||||
value: string;
|
||||
name: string;
|
||||
visibility: EnvironmentVariableVisibility;
|
||||
environments: EnvironmentVariableEnvironment[];
|
||||
type: EnvironmentSecretType;
|
||||
isGlobal?: boolean;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
export type EnvironmentVariablePushInput = {
|
||||
name: string;
|
||||
value: string;
|
||||
environments: EnvironmentVariableEnvironment[];
|
||||
visibility: EnvironmentVariableVisibility;
|
||||
overwrite?: boolean;
|
||||
};
|
||||
|
||||
export const EnvironmentVariableMutation = {
|
||||
async createSharedVariableAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: CreateVariableArgs,
|
||||
input: CreateSharedEnvironmentVariableInput,
|
||||
accountId: string
|
||||
): Promise<EnvironmentVariableFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
@@ -69,7 +51,7 @@ export const EnvironmentVariableMutation = {
|
||||
},
|
||||
async createForAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: CreateVariableArgs,
|
||||
input: CreateEnvironmentVariableInput,
|
||||
appId: string
|
||||
): Promise<EnvironmentVariableFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
@@ -98,7 +80,7 @@ export const EnvironmentVariableMutation = {
|
||||
},
|
||||
async updateAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: Partial<CreateVariableArgs> & { id: string }
|
||||
input: UpdateEnvironmentVariableInput
|
||||
): Promise<EnvironmentVariableFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -143,7 +125,7 @@ export const EnvironmentVariableMutation = {
|
||||
},
|
||||
async createBulkEnvironmentVariablesForAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: EnvironmentVariablePushInput[],
|
||||
input: CreateEnvironmentVariableInput[],
|
||||
appId: string
|
||||
): Promise<boolean> {
|
||||
await withErrorHandlingAsync(
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { FingerprintSource } from '@expo/eas-build-job';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import { CreateFingeprintMutation, FingerprintFragment } from '../generated';
|
||||
import {
|
||||
CreateFingeprintMutation,
|
||||
FingerprintFragment,
|
||||
FingerprintSourceInput,
|
||||
} from '../generated';
|
||||
import { FingerprintFragmentNode } from '../types/Fingerprint';
|
||||
|
||||
export const FingerprintMutation = {
|
||||
async createFingerprintAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
fingerprintData: { hash: string; source?: FingerprintSource }
|
||||
fingerprintData: { hash: string; source?: FingerprintSourceInput }
|
||||
): Promise<FingerprintFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
|
||||
@@ -4,8 +4,11 @@ import { z } from 'zod';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
GetOrCreateWorkflowRevisionFromGitRefMutation,
|
||||
GetOrCreateWorkflowRevisionFromGitRefMutationVariables,
|
||||
ValidateWorkflowYamlConfigMutation,
|
||||
ValidateWorkflowYamlConfigMutationVariables,
|
||||
WorkflowRevision,
|
||||
} from '../generated';
|
||||
|
||||
export namespace WorkflowRevisionMutation {
|
||||
@@ -17,6 +20,61 @@ export namespace WorkflowRevisionMutation {
|
||||
}),
|
||||
});
|
||||
|
||||
export async function getOrCreateWorkflowRevisionFromGitRefAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
fileName,
|
||||
gitRef,
|
||||
}: {
|
||||
appId: string;
|
||||
fileName: string;
|
||||
gitRef: string;
|
||||
}
|
||||
): Promise<WorkflowRevision | undefined> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
GetOrCreateWorkflowRevisionFromGitRefMutation,
|
||||
GetOrCreateWorkflowRevisionFromGitRefMutationVariables
|
||||
>(
|
||||
gql`
|
||||
mutation GetOrCreateWorkflowRevisionFromGitRef(
|
||||
$appId: ID!
|
||||
$fileName: String!
|
||||
$gitRef: String!
|
||||
) {
|
||||
workflowRevision {
|
||||
getOrCreateWorkflowRevisionFromGitRef(
|
||||
appId: $appId
|
||||
fileName: $fileName
|
||||
gitRef: $gitRef
|
||||
) {
|
||||
id
|
||||
yamlConfig
|
||||
blobSha
|
||||
commitSha
|
||||
createdAt
|
||||
workflow {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
fileName,
|
||||
gitRef,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return (
|
||||
(data.workflowRevision?.getOrCreateWorkflowRevisionFromGitRef as WorkflowRevision) ??
|
||||
undefined
|
||||
);
|
||||
}
|
||||
export async function validateWorkflowYamlConfigAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
CancelWorkflowRunMutation,
|
||||
CancelWorkflowRunMutationVariables,
|
||||
CreateWorkflowRunFromGitRefMutation,
|
||||
CreateWorkflowRunFromGitRefMutationVariables,
|
||||
CreateWorkflowRunMutation,
|
||||
CreateWorkflowRunMutationVariables,
|
||||
WorkflowRevisionInput,
|
||||
@@ -54,6 +56,53 @@ export namespace WorkflowRunMutation {
|
||||
);
|
||||
return { id: data.workflowRun.createWorkflowRun.id };
|
||||
}
|
||||
|
||||
export async function createWorkflowRunFromGitRefAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
workflowRevisionId,
|
||||
gitRef,
|
||||
inputs,
|
||||
}: {
|
||||
workflowRevisionId: string;
|
||||
gitRef: string;
|
||||
inputs?: Record<string, any>;
|
||||
}
|
||||
): Promise<{ id: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
CreateWorkflowRunFromGitRefMutation,
|
||||
CreateWorkflowRunFromGitRefMutationVariables
|
||||
>(
|
||||
gql`
|
||||
mutation CreateWorkflowRunFromGitRef(
|
||||
$workflowRevisionId: ID!
|
||||
$gitRef: String!
|
||||
$inputs: JSONObject
|
||||
) {
|
||||
workflowRun {
|
||||
createWorkflowRunFromGitRef(
|
||||
workflowRevisionId: $workflowRevisionId
|
||||
gitRef: $gitRef
|
||||
inputs: $inputs
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
workflowRevisionId,
|
||||
gitRef,
|
||||
inputs,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return { id: data.workflowRun.createWorkflowRunFromGitRef.id };
|
||||
}
|
||||
|
||||
export async function cancelWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AccountUsageForOverageWarningQuery,
|
||||
AccountUsageForOverageWarningQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const AccountUsageQuery = {
|
||||
async getUsageForOverageWarningAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date
|
||||
): Promise<AccountUsageForOverageWarningQuery['account']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountUsageForOverageWarningQuery, AccountUsageForOverageWarningQueryVariables>(
|
||||
gql`
|
||||
query AccountUsageForOverageWarning($accountId: String!, $currentDate: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
}
|
||||
usageMetrics {
|
||||
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId, currentDate: currentDate.toISOString() },
|
||||
{
|
||||
additionalTypenames: ['Account', 'AccountUsageMetrics', 'UsageMetricTotal'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byId;
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import gql from 'graphql-tag';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
EnvironmentVariableEnvironment,
|
||||
EnvironmentVariableFragment,
|
||||
EnvironmentVariablesByAppIdQuery,
|
||||
EnvironmentVariablesSharedQuery,
|
||||
@@ -14,7 +13,7 @@ import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
|
||||
import { EnvironmentVariableWithSecretFragmentNode } from '../types/EnvironmentVariableWithSecret';
|
||||
|
||||
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
|
||||
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
|
||||
linkedEnvironments?: string[] | null;
|
||||
};
|
||||
|
||||
export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
|
||||
@@ -22,6 +21,31 @@ export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
|
||||
};
|
||||
|
||||
export const EnvironmentVariablesQuery = {
|
||||
async environmentVariableEnvironmentsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string
|
||||
): Promise<string[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query(
|
||||
gql`
|
||||
query AppEnvironmentVariableEnvironments($appId: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
environmentVariableEnvironments
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId },
|
||||
{ additionalTypenames: ['App'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app?.byId.environmentVariableEnvironments ?? [];
|
||||
},
|
||||
async byAppIdWithSensitiveAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
@@ -31,7 +55,7 @@ export const EnvironmentVariablesQuery = {
|
||||
includeFileContent = false,
|
||||
}: {
|
||||
appId: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
filterNames?: string[];
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
@@ -80,7 +104,7 @@ export const EnvironmentVariablesQuery = {
|
||||
includeFileContent = false,
|
||||
}: {
|
||||
appId: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
filterNames?: string[];
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
@@ -128,7 +152,7 @@ export const EnvironmentVariablesQuery = {
|
||||
}: {
|
||||
appId: string;
|
||||
filterNames?: string[];
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> {
|
||||
@@ -178,7 +202,7 @@ export const EnvironmentVariablesQuery = {
|
||||
}: {
|
||||
appId: string;
|
||||
filterNames?: string[];
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
): Promise<EnvironmentVariableWithFileContent[]> {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const BuildFragmentNode = gql`
|
||||
id
|
||||
displayName
|
||||
}
|
||||
logFiles
|
||||
project {
|
||||
__typename
|
||||
id
|
||||
|
||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user