mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b601de88 | ||
|
|
c5867c8c07 | ||
|
|
f2d5c210a7 | ||
|
|
6e70f7ee4b | ||
|
|
1d36f4f636 | ||
|
|
f8deba78a5 | ||
|
|
df8ebc8f84 | ||
|
|
62ab329f39 | ||
|
|
198b796804 | ||
|
|
110055d943 | ||
|
|
06e2ba5bdb | ||
|
|
15690e7f6b | ||
|
|
134e74f50b | ||
|
|
4b3dcda99d | ||
|
|
0c32aa3a39 | ||
|
|
59ac8ae77a | ||
|
|
3e13df470e | ||
|
|
83877f6285 | ||
|
|
56018338e7 | ||
|
|
0fdf2dd44a | ||
|
|
0dcd6d465a | ||
|
|
d5da4f4167 | ||
|
|
d0b9281bb2 | ||
|
|
77e04a3bad | ||
|
|
92ff6a0da5 | ||
|
|
25d945b63f | ||
|
|
37165428d1 | ||
|
|
50b9be975e | ||
|
|
2655482f74 | ||
|
|
dc103acf06 | ||
|
|
b87bc5bf8f | ||
|
|
1604b7bc20 | ||
|
|
13a6356fca | ||
|
|
0823a9d675 |
No files matched your search
@@ -32,11 +32,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
run: yarn install --immutable
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
run: yarn run -T lerna publish from-package --yes --no-verify-access
|
||||
- name: Add latest-eas-build-staging tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
@@ -64,12 +64,12 @@ jobs:
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
run: yarn install --immutable
|
||||
- name: Resolve version
|
||||
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Update changelog
|
||||
working-directory: ./scripts
|
||||
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||
run: yarn release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||
- name: Publish release
|
||||
run: gh release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
||||
env:
|
||||
|
||||
@@ -20,6 +20,6 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- run: yarn verify-graphql-code
|
||||
working-directory: packages/eas-cli
|
||||
@@ -24,5 +24,5 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn install --immutable
|
||||
- run: yarn typecheck
|
||||
+18
-10
@@ -22,11 +22,14 @@ jobs:
|
||||
name: Test with Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- name: Setup tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
env:
|
||||
MISE_NODE_VERSION: ${{ matrix.node }}
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
env:
|
||||
YARN_ENABLE_HARDENED_MODE: ${{ matrix.coverage == true && '1' || '0' }}
|
||||
- run: yarn typecheck
|
||||
- run: |
|
||||
git config --global user.email "[email protected]"
|
||||
@@ -38,32 +41,37 @@ jobs:
|
||||
- name: Run tests (affected)
|
||||
# PRs: only run tests for changed packages + dependents.
|
||||
if: ${{ !matrix.coverage && github.event_name == 'pull_request' }}
|
||||
run: yarn lerna run test --since ${{ github.event.pull_request.base.sha }} --include-dependents
|
||||
run: yarn run -T lerna run test --since ${{ github.event.pull_request.base.sha }} --include-dependents
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
- name: Run tests (all)
|
||||
# Pushes to main: run all package tests.
|
||||
if: ${{ !matrix.coverage && github.event_name == 'push' }}
|
||||
run: yarn lerna run test
|
||||
run: yarn run -T lerna run test
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
- name: Run tests (coverage)
|
||||
# Coverage on PRs and main; per-package reports are uploaded to Codecov.
|
||||
if: ${{ matrix.coverage }}
|
||||
run: yarn lerna run test -- -- --coverage
|
||||
run: yarn run -T lerna run test -- --coverage
|
||||
env:
|
||||
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
||||
# Remove this whenever we solved those OOM errors.
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
- run: yarn lint
|
||||
- run: yarn fmt:check
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- name: Install scripts dependencies
|
||||
if: ${{ matrix.coverage }}
|
||||
run: yarn install --immutable
|
||||
working-directory: ./scripts
|
||||
env:
|
||||
YARN_ENABLE_HARDENED_MODE: ${{ matrix.coverage == true && '1' || '0' }}
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- run: yarn lint-changelog
|
||||
- if: ${{ matrix.coverage }}
|
||||
run: yarn lint-changelog
|
||||
|
||||
notify-slack:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
yarn install --frozen-lockfile --check-files
|
||||
yarn install --immutable
|
||||
yarn build
|
||||
if [[ "$INPUT_VERSION" == "" ]]; then
|
||||
echo "Releasing with default version"
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/setup-mise
|
||||
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn install --immutable
|
||||
|
||||
- run: yarn typecheck
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ dist
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
**/.yarn/install-state.gz
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
"endOfLine": "lf",
|
||||
"ignorePatterns": [
|
||||
".github",
|
||||
"lerna.json",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/coverage/**",
|
||||
"**/fixtures/**",
|
||||
"packages/eas-cli/README.md",
|
||||
"packages/eas-cli/schema/**",
|
||||
"packages/eas-cli/graphql.schema.json",
|
||||
"packages/build-tools/schema.graphql"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
defaultSemverRangePrefix: ''
|
||||
nodeLinker: node-modules
|
||||
@@ -6,6 +6,41 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support YAML anchors and aliases when parsing workflow files. ([#3435](https://github.com/expo/eas-cli/pull/3435) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Remove --environment flag requirement for update:configure. ([#3440](https://github.com/expo/eas-cli/pull/3440) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix login spinner interfering with prompts in `eas go` command when not logged in. ([#3451](https://github.com/expo/eas-cli/pull/3451) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.0.6](https://github.com/expo/eas-cli/releases/tag/v18.0.6) - 2026-02-27
|
||||
|
||||
## [18.0.5](https://github.com/expo/eas-cli/releases/tag/v18.0.5) - 2026-02-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require --environment flag for update commands when SDK >= 55. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [18.0.4](https://github.com/expo/eas-cli/releases/tag/v18.0.4) - 2026-02-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert authorization code flow with PKCE for browser-based login. ([#3422](https://github.com/expo/eas-cli/pull/3422) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
## [18.0.3](https://github.com/expo/eas-cli/releases/tag/v18.0.3) - 2026-02-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
## [18.0.1](https://github.com/expo/eas-cli/releases/tag/v18.0.1) - 2026-02-12
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for Node 18 and Node 19. Require Node 20+. ([#3343](https://github.com/expo/eas-cli/pull/3343) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
@@ -98,8 +98,8 @@ yarn start-allow-unused
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
yarn lerna run test # Run all tests across packages
|
||||
yarn lerna run test -- --watch # Watch mode across packages (heavier)
|
||||
yarn run -T lerna run test # Run all tests across packages
|
||||
yarn run -T lerna run test -- --watch # Watch mode across packages (heavier)
|
||||
#
|
||||
# If you use `yarn test`, remember to pass args through two separators:
|
||||
# yarn test -- -- --watch
|
||||
@@ -349,5 +349,5 @@ See `LICENSE` (MIT) and `LICENSE-BUSL` (BUSL-1.1) for details.
|
||||
## Important Notes
|
||||
|
||||
- **Node Version**: Requires Node.js (managed via Mise)
|
||||
- **Package Manager**: Uses Yarn 1.22.21
|
||||
- **Package Manager**: Uses Yarn 4.12.0
|
||||
- **Compilation Target**: CommonJS with Node resolution
|
||||
+1
-1
@@ -69,7 +69,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for how to set up your environment when making ch
|
||||
|
||||
## Testing
|
||||
|
||||
From the repository root, run `yarn lerna run test`. For a single package, run `yarn test` in that package directory.
|
||||
From the repository root, run `yarn run -T lerna run test`. For a single package, run `yarn test` in that package directory.
|
||||
|
||||
## Releasing
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "18.0.1",
|
||||
"version": "18.1.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
[env]
|
||||
_.path = "./tools/bin"
|
||||
MISE_NODE_COREPACK = "true"
|
||||
|
||||
[tools]
|
||||
node = "20.19.4"
|
||||
yarn = "1.22.22"
|
||||
+3
-2
@@ -15,7 +15,7 @@
|
||||
"fmt": "oxfmt .",
|
||||
"fmt:check": "oxfmt --check .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "lerna run rebuild && ./scripts/bin/run release",
|
||||
"release": "lerna run rebuild --concurrency 1 && ./scripts/bin/run release",
|
||||
"test": "lerna run test",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
@@ -32,5 +32,6 @@
|
||||
"oxlint-tsgolint": "0.11.1",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
},
|
||||
"packageManager": "[email protected]"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "18.0.1",
|
||||
"version": "18.1.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -38,20 +38,22 @@
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.0.1",
|
||||
"@expo/steps": "18.0.2",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
"@google-cloud/storage": "^7.11.2",
|
||||
"@urql/core": "^6.0.1",
|
||||
"bplist-parser": "0.3.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fast-xml-parser": "^4.4.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"gql.tada": "^1.8.13",
|
||||
"joi": "^17.13.1",
|
||||
@@ -59,6 +61,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"plist": "^3.1.0",
|
||||
"promise-limit": "^2.7.0",
|
||||
|
||||
@@ -17,7 +17,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
`Your project requires a newer version of CocoaPods. You can update it in the build profile in eas.json by either:
|
||||
- changing the current version under key "cocoapods"
|
||||
- switching to an image that supports that version under key "image"`,
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
{ docsUrl: 'https://docs.expo.dev/build-reference/eas-json/' }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -30,7 +30,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR',
|
||||
`Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.`,
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
{ docsUrl: 'https://docs.expo.dev/build-reference/eas-json/' }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -70,7 +70,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -85,7 +85,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -98,7 +98,7 @@ export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_PLIST_ERROR',
|
||||
'"GoogleService-Info.plist" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
{ docsUrl: 'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' }
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -234,13 +234,19 @@ You are seeing this error because either:
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or upgrade to SDK 46 or higher.`,
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations'
|
||||
{
|
||||
docsUrl:
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations',
|
||||
}
|
||||
)
|
||||
: new UserFacingError(
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or turn off signing resource bundles in your Podfile: https://expo.fyi/r/disable-bundle-resource-signing`,
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations'
|
||||
{
|
||||
docsUrl:
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildRuntimePlatform, ExternalBuildContextProvider } from '@expo/steps';
|
||||
import { Client } from '@urql/core';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
@@ -53,6 +54,7 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
public readonly startTime: Date;
|
||||
|
||||
public readonly logger: bunyan;
|
||||
public readonly graphqlClient: Client;
|
||||
public readonly runtimeApi: BuilderRuntimeApi;
|
||||
public job: TJob;
|
||||
public metadata?: Metadata;
|
||||
@@ -65,6 +67,7 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
|
||||
this.metadata = buildCtx.metadata;
|
||||
|
||||
this.logger = buildCtx.logger.child({ phase: BuildPhase.CUSTOM });
|
||||
this.graphqlClient = buildCtx.graphqlClient;
|
||||
this.projectSourceDirectory = path.join(buildCtx.workingdir, 'temporary-custom-build');
|
||||
this.projectTargetDirectory = path.join(buildCtx.workingdir, 'build');
|
||||
this.defaultWorkingDirectory = buildCtx.getReactNativeProjectDirectory();
|
||||
|
||||
@@ -19,7 +19,9 @@ import { createInstallNodeModulesBuildFunction } from './functions/installNodeMo
|
||||
import { createInstallPodsBuildFunction } from './functions/installPods';
|
||||
import { createInternalEasMaestroTestFunction } from './functions/internalMaestroTest';
|
||||
import { createPrebuildBuildFunction } from './functions/prebuild';
|
||||
import { createReadIpaInfoBuildFunction } from './functions/readIpaInfo';
|
||||
import { createRepackBuildFunction } from './functions/repack';
|
||||
import { createReportMaestroTestResultsFunction } from './functions/reportMaestroTestResults';
|
||||
import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials';
|
||||
import { createResolveBuildConfigBuildFunction } from './functions/resolveBuildConfig';
|
||||
import {
|
||||
@@ -48,6 +50,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
createSetUpNpmrcBuildFunction(),
|
||||
createInstallNodeModulesBuildFunction(),
|
||||
createPrebuildBuildFunction(),
|
||||
createReadIpaInfoBuildFunction(),
|
||||
createDownloadBuildFunction(),
|
||||
createRepackBuildFunction(),
|
||||
createRestoreCacheFunction(),
|
||||
@@ -79,6 +82,8 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
createUploadToAscBuildFunction(),
|
||||
|
||||
createInternalEasMaestroTestFunction(ctx),
|
||||
|
||||
createReportMaestroTestResultsFunction(ctx),
|
||||
];
|
||||
|
||||
if (ctx.hasBuildJob()) {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,98 @@
|
||||
import { spawnAsync } from '@expo/steps';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { IosSimulatorUtils } from '../../../utils/IosSimulatorUtils';
|
||||
import { findMaestroPathsFlowsToExecuteAsync } from '../../../utils/findMaestroPathsFlowsToExecuteAsync';
|
||||
import { createInternalEasMaestroTestFunction } from '../internalMaestroTest';
|
||||
|
||||
jest.mock('@expo/steps', () => ({
|
||||
...jest.requireActual('@expo/steps'),
|
||||
spawnAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/IosSimulatorUtils');
|
||||
jest.mock('../../../utils/AndroidEmulatorUtils');
|
||||
jest.mock('../../../utils/findMaestroPathsFlowsToExecuteAsync');
|
||||
|
||||
const mockedSpawnAsync = jest.mocked(spawnAsync);
|
||||
const mockedIosUtils = jest.mocked(IosSimulatorUtils);
|
||||
const mockedFindFlows = jest.mocked(findMaestroPathsFlowsToExecuteAsync);
|
||||
|
||||
describe(createInternalEasMaestroTestFunction, () => {
|
||||
const mockUploadArtifact = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vol.mkdirSync(os.tmpdir(), { recursive: true });
|
||||
|
||||
mockedSpawnAsync.mockResolvedValue(undefined as any);
|
||||
mockUploadArtifact.mockResolvedValue({ artifactId: null });
|
||||
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([
|
||||
{ name: 'iPhone 15', udid: 'test-udid-123' } as any,
|
||||
]);
|
||||
mockedIosUtils.cloneAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.startAsync.mockResolvedValue({ udid: 'cloned-udid' } as any);
|
||||
mockedIosUtils.waitForReadyAsync.mockResolvedValue(undefined as any);
|
||||
mockedIosUtils.collectLogsAsync.mockResolvedValue({ outputPath: '/tmp/logs.txt' } as any);
|
||||
mockedIosUtils.deleteAsync.mockResolvedValue(undefined as any);
|
||||
|
||||
mockedFindFlows.mockResolvedValue(['/project/.maestro/home.yml']);
|
||||
});
|
||||
|
||||
function createStep(overrides?: { callInputs?: Record<string, unknown> }) {
|
||||
const ctx = {
|
||||
runtimeApi: { uploadArtifact: mockUploadArtifact },
|
||||
};
|
||||
const fn = createInternalEasMaestroTestFunction(ctx as any);
|
||||
return fn.createBuildStepFromFunctionCall(
|
||||
createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
}),
|
||||
{
|
||||
callInputs: {
|
||||
platform: 'ios',
|
||||
flow_paths: JSON.stringify(['.maestro']),
|
||||
...overrides?.callInputs,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('sets junit_report_directory output when output_format is junit', async () => {
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toMatch(/maestro-reports-/);
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory output when output_format is not junit', async () => {
|
||||
const step = createStep();
|
||||
await step.executeAsync();
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not set junit_report_directory when function throws before reaching output code', async () => {
|
||||
mockedIosUtils.getAvailableDevicesAsync.mockResolvedValue([]);
|
||||
|
||||
const step = createStep({
|
||||
callInputs: { output_format: 'junit' },
|
||||
});
|
||||
|
||||
try {
|
||||
await step.executeAsync();
|
||||
} catch {
|
||||
// Expected - no booted device found
|
||||
}
|
||||
|
||||
const junitDir = step.getOutputValueByName('junit_report_directory');
|
||||
expect(junitDir).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,594 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import {
|
||||
parseFlowMetadata,
|
||||
parseJUnitTestCases,
|
||||
parseMaestroResults,
|
||||
} from '../maestroResultParser';
|
||||
|
||||
describe(parseFlowMetadata, () => {
|
||||
it('parses valid ai-*.json', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toEqual({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when flow_name is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_file_path: '/Users/expo/workingdir/build/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when flow_file_path is missing', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid JSON', async () => {
|
||||
vol.fromJSON({
|
||||
'/tests/2026-01-28_055409/ai-home.json': 'not json',
|
||||
});
|
||||
|
||||
const result = await parseFlowMetadata('/tests/2026-01-28_055409/ai-home.json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseMaestroResults, () => {
|
||||
it('parses JUnit results and enriches with ai-*.json metadata', async () => {
|
||||
vol.fromJSON({
|
||||
// JUnit XML (primary data)
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="2" failures="1">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.5" status="SUCCESS"/>',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Debug output (for flow_file_path)
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
|
||||
flow_name: 'login',
|
||||
flow_file_path: '/root/project/.maestro/login.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'passed',
|
||||
errorMessage: null,
|
||||
duration: 10500,
|
||||
retryCount: 0,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '.maestro/login.yml',
|
||||
status: 'failed',
|
||||
errorMessage: 'Tap failed',
|
||||
duration: 5000,
|
||||
retryCount: 0,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates retryCount from timestamp directory occurrences', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Two timestamp dirs = 1 retry
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
'/tests/2026-01-28_055420/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].retryCount).toBe(1);
|
||||
});
|
||||
|
||||
it('uses flow name as fallback path when ai-*.json not found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// No ai-*.json files
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].path).toBe('home');
|
||||
expect(results[0].retryCount).toBe(0);
|
||||
});
|
||||
|
||||
it('returns empty array when no JUnit files found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/.gitkeep': '',
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts tags from JUnit properties and separates from other properties', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="env" value="staging"/>',
|
||||
' <property name="tags" value="e2e, smoke"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].tags).toEqual(['e2e', 'smoke']);
|
||||
expect(results[0].properties).toEqual({ env: 'staging' });
|
||||
});
|
||||
|
||||
it('handles reuse_devices=true (junit_report_directory == tests_directory)', async () => {
|
||||
vol.fromJSON({
|
||||
// Same directory for both JUnit and debug output
|
||||
'/maestro-tests/android-maestro-junit.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/maestro-tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/maestro-tests', '/maestro-tests', '/root/project');
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'passed',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles reuse_devices=false (separate junit_report_directory)', async () => {
|
||||
vol.fromJSON({
|
||||
// JUnit in temp dir (per-flow files)
|
||||
'/tmp/maestro-reports-abc123/junit-report-flow-1.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tmp/maestro-reports-abc123/junit-report-flow-2.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
// Debug output in default location
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
|
||||
flow_name: 'login',
|
||||
flow_file_path: '/root/project/.maestro/login.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults(
|
||||
'/tmp/maestro-reports-abc123',
|
||||
'/tests',
|
||||
'/root/project'
|
||||
);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'home', path: '.maestro/home.yml', status: 'passed' }),
|
||||
expect.objectContaining({ name: 'login', path: '.maestro/login.yml', status: 'failed' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('uses raw path when flow_file_path is outside project_root', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/somewhere/else/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
expect(results[0].path).toBe('/somewhere/else/.maestro/home.yml');
|
||||
});
|
||||
|
||||
it('filters out non-timestamp directories when scanning debug output', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
'/tests/not-a-timestamp/ai-home.json': JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
const results = await parseMaestroResults('/junit', '/tests', '/root/project');
|
||||
// Only 1 occurrence (not-a-timestamp dir should be ignored)
|
||||
expect(results[0].retryCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseJUnitTestCases, () => {
|
||||
it('parses a single testcase with SUCCESS status', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" device="emulator-5554" tests="1" failures="0" time="10.5">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.5" status="SUCCESS">',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'home',
|
||||
status: 'passed',
|
||||
duration: 10500,
|
||||
errorMessage: null,
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses a failed testcase with failure message', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Element not visible</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: 'login',
|
||||
status: 'failed',
|
||||
duration: 5000,
|
||||
errorMessage: 'Element not visible',
|
||||
tags: [],
|
||||
properties: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('extracts properties from testcase', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="env" value="staging"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].properties).toEqual({ env: 'staging', priority: 'high' });
|
||||
});
|
||||
|
||||
it('extracts tags from "tags" property as comma-separated values', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="tags" value="smoke, critical, auth"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].tags).toEqual(['smoke', 'critical', 'auth']);
|
||||
expect(results[0].properties).toEqual({ priority: 'high' });
|
||||
});
|
||||
|
||||
it('returns empty tags when no "tags" property exists (older Maestro)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].tags).toEqual([]);
|
||||
expect(results[0].properties).toEqual({ priority: 'high' });
|
||||
});
|
||||
|
||||
it('parses multiple testcases across multiple testsuites (shards)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" device="emulator-5554" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
' <testsuite name="Test Suite" device="emulator-5556" tests="1" failures="1">',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'home', status: 'passed' }),
|
||||
expect.objectContaining({ name: 'login', status: 'failed', errorMessage: 'Tap failed' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('parses multiple JUnit XML files in the same directory', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/junit-report-flow-1.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/junit/junit-report-flow-2.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles missing time attribute (defaults to 0)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].duration).toBe(0);
|
||||
});
|
||||
|
||||
it('handles invalid time attribute (defaults to 0)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="not_a_number" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].duration).toBe(0);
|
||||
});
|
||||
|
||||
it('uses @_status attribute for pass/fail (not <failure> presence)', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].status).toBe('failed');
|
||||
expect(results[0].errorMessage).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts error message from <error> element', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR">',
|
||||
' <error>Runtime exception occurred</error>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].status).toBe('failed');
|
||||
expect(results[0].errorMessage).toBe('Runtime exception occurred');
|
||||
});
|
||||
|
||||
it('returns empty array when directory does not exist', async () => {
|
||||
const results = await parseJUnitTestCases('/nonexistent');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when no XML files found', async () => {
|
||||
vol.fromJSON({ '/junit/.gitkeep': '' });
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips invalid XML files gracefully', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/bad.xml': 'not xml at all',
|
||||
'/junit/good.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results).toEqual([expect.objectContaining({ name: 'home', status: 'passed' })]);
|
||||
});
|
||||
|
||||
it('handles testcase with no properties element', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const results = await parseJUnitTestCases('/junit');
|
||||
expect(results[0].properties).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
jest.unmock('fs');
|
||||
jest.unmock('node:fs');
|
||||
jest.unmock('fs/promises');
|
||||
jest.unmock('node:fs/promises');
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createReadIpaInfoBuildFunction, readIpaInfoAsync } from '../readIpaInfo';
|
||||
|
||||
const NO_INFO_PLIST_IPA_BASE64 =
|
||||
'UEsDBAoAAAAAAPRcUFwAAAAAAAAAAAAAAAAIABwAUGF5bG9hZC9VVAkAA+zzkmns85JpdXgLAAEE9QEAAAQUAAAAUEsDBAoAAAAAAPRcUFwAAAAAAAAAAAAAAAAUABwAUGF5bG9hZC9UZXN0QXBwLmFwcC9VVAkAA+zzkmns85JpdXgLAAEE9QEAAAQUAAAAUEsDBAoAAAAAAPRcUFwgMDo2BgAAAAYAAAAeABwAUGF5bG9hZC9UZXN0QXBwLmFwcC9SZWFkbWUudHh0VVQJAAPs85Jp7POSaXV4CwABBPUBAAAEFAAAAGhlbGxvClBLAQIeAwoAAAAAAPRcUFwAAAAAAAAAAAAAAAAIABgAAAAAAAAAEADtQQAAAABQYXlsb2FkL1VUBQAD7POSaXV4CwABBPUBAAAEFAAAAFBLAQIeAwoAAAAAAPRcUFwAAAAAAAAAAAAAAAAUABgAAAAAAAAAEADtQUIAAABQYXlsb2FkL1Rlc3RBcHAuYXBwL1VUBQAD7POSaXV4CwABBPUBAAAEFAAAAFBLAQIeAwoAAAAAAPRcUFwgMDo2BgAAAAYAAAAeABgAAAAAAAEAAACkgZAAAABQYXlsb2FkL1Rlc3RBcHAuYXBwL1JlYWRtZS50eHRVVAUAA+zzkml1eAsAAQT1AQAABBQAAABQSwUGAAAAAAMAAwAMAQAA7gAAAAAA';
|
||||
const MISSING_BUNDLE_VERSION_IPA_BASE64 =
|
||||
'UEsDBAoAAAAAAPRcUFwAAAAAAAAAAAAAAAAIABwAUGF5bG9hZC9VVAkAA+zzkmns85JpdXgLAAEE9QEAAAQUAAAAUEsDBAoAAAAAAPRcUFwAAAAAAAAAAAAAAAAUABwAUGF5bG9hZC9UZXN0QXBwLmFwcC9VVAkAA+zzkmns85JpdXgLAAEE9QEAAAQUAAAAUEsDBBQAAAAIAPRcUFz9BfqM2wAAAEQBAAAeABwAUGF5bG9hZC9UZXN0QXBwLmFwcC9JbmZvLnBsaXN0VVQJAAPs85Jp7POSaXV4CwABBPUBAAAEFAAAAGWOUUvDMBSF3/srYt6b6/RFJMtw7QaFooV2go+jybZgm4Tkard/b7IKor7ec+73Hb46jwP5VD5oa5Z0wW4pUaa3Upvjku66bf5AVyLjN+VL0b01G+IGHZA0u3VdFYTmAE/ODQqg7ErS1FXbkcgA2DxTQk+I7hFgmia2Ty3W2zEVAzTeOuXxUkdYHh+YREmjZqb/mhOvUvcoMkL4u7qIYrv+MHJQlVQG9UErzyHdUx7Qx90iapg678ekRBUwyjl8Z38x7cl6fJ2F7bXyD7dgd+z+B8Bh3sPhulZkX1BLAQIeAwoAAAAAAPRcUFwAAAAAAAAAAAAAAAAIABgAAAAAAAAAEADtQQAAAABQYXlsb2FkL1VUBQAD7POSaXV4CwABBPUBAAAEFAAAAFBLAQIeAwoAAAAAAPRcUFwAAAAAAAAAAAAAAAAUABgAAAAAAAAAEADtQUIAAABQYXlsb2FkL1Rlc3RBcHAuYXBwL1VUBQAD7POSaXV4CwABBPUBAAAEFAAAAFBLAQIeAxQAAAAIAPRcUFz9BfqM2wAAAEQBAAAeABgAAAAAAAEAAACkgZAAAABQYXlsb2FkL1Rlc3RBcHAuYXBwL0luZm8ucGxpc3RVVAUAA+zzkml1eAsAAQT1AQAABBQAAABQSwUGAAAAAAMAAwAMAQAAwwEAAAAA';
|
||||
const IPA_FIXTURE_SOURCE_PATH = path.join(__dirname, 'fixtures/SmallestAppExample.ipa');
|
||||
|
||||
async function writeIpaFileAsync(filePath: string, base64Data: string): Promise<void> {
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.writeFile(filePath, base64Data, { encoding: 'base64' });
|
||||
}
|
||||
|
||||
describe(readIpaInfoAsync, () => {
|
||||
it('reads bundle identifier and versions from fixture ipa', async () => {
|
||||
await expect(readIpaInfoAsync(IPA_FIXTURE_SOURCE_PATH)).resolves.toEqual({
|
||||
bundleIdentifier: 'dev.expo.SmallestAppExample',
|
||||
bundleShortVersion: '1.0',
|
||||
bundleVersion: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when Info.plist cannot be found in IPA', async () => {
|
||||
const ipaPath = '/tmp/no-info.ipa';
|
||||
await writeIpaFileAsync(ipaPath, NO_INFO_PLIST_IPA_BASE64);
|
||||
|
||||
await expect(readIpaInfoAsync(ipaPath)).rejects.toThrow(
|
||||
`Failed to read IPA info: Could not find Info.plist in ${ipaPath}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when required plist fields are missing', async () => {
|
||||
const ipaPath = '/tmp/missing-bundle-version.ipa';
|
||||
await writeIpaFileAsync(ipaPath, MISSING_BUNDLE_VERSION_IPA_BASE64);
|
||||
|
||||
await expect(readIpaInfoAsync(ipaPath)).rejects.toThrow(
|
||||
/Failed to read IPA info: .*CFBundleVersion/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(createReadIpaInfoBuildFunction, () => {
|
||||
it('sets build step outputs', async () => {
|
||||
const readIpaInfo = createReadIpaInfoBuildFunction();
|
||||
const globalContext = createGlobalContextMock({
|
||||
staticContextContent: {
|
||||
job: {},
|
||||
},
|
||||
});
|
||||
|
||||
const buildStep = readIpaInfo.createBuildStepFromFunctionCall(globalContext, {
|
||||
callInputs: {
|
||||
ipa_path: IPA_FIXTURE_SOURCE_PATH,
|
||||
},
|
||||
});
|
||||
await buildStep.executeAsync();
|
||||
|
||||
expect(buildStep.outputById.bundle_identifier.value).toBe('dev.expo.SmallestAppExample');
|
||||
expect(buildStep.outputById.bundle_short_version.value).toBe('1.0');
|
||||
expect(buildStep.outputById.bundle_version.value).toBe('1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Client } from '@urql/core';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { createMockLogger } from '../../../__tests__/utils/logger';
|
||||
import { createReportMaestroTestResultsFunction } from '../reportMaestroTestResults';
|
||||
|
||||
const JUNIT_PASS = [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n');
|
||||
|
||||
const JUNIT_FAIL = [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="1">',
|
||||
' <testcase id="home" name="home" classname="home" time="5.0" status="ERROR">',
|
||||
' <failure>Tap failed</failure>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n');
|
||||
|
||||
const FLOW_AI = JSON.stringify({
|
||||
flow_name: 'home',
|
||||
flow_file_path: '/root/project/.maestro/home.yml',
|
||||
});
|
||||
|
||||
describe(createReportMaestroTestResultsFunction, () => {
|
||||
let mockMutationFn: jest.Mock;
|
||||
let mockGraphqlClient: Client;
|
||||
|
||||
beforeEach(() => {
|
||||
mockMutationFn = jest.fn();
|
||||
mockGraphqlClient = {
|
||||
mutation: jest.fn().mockReturnValue({
|
||||
toPromise: mockMutationFn,
|
||||
}),
|
||||
} as unknown as Client;
|
||||
});
|
||||
|
||||
function createStep(overrides?: {
|
||||
callInputs?: Record<string, unknown>;
|
||||
staticContextContent?: Record<string, unknown>;
|
||||
env?: Record<string, string>;
|
||||
}) {
|
||||
const ctx = { graphqlClient: mockGraphqlClient };
|
||||
const fn = createReportMaestroTestResultsFunction(ctx as any);
|
||||
const globalCtx = createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
projectTargetDirectory: '/root/project',
|
||||
staticContextContent: {
|
||||
expoApiServerURL: 'https://api.expo.test',
|
||||
job: { secrets: { robotAccessToken: 'test-token' } },
|
||||
...overrides?.staticContextContent,
|
||||
},
|
||||
});
|
||||
globalCtx.updateEnv(overrides?.env ?? { __WORKFLOW_JOB_ID: 'job-uuid-123' });
|
||||
return fn.createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {
|
||||
junit_report_directory: '/junit',
|
||||
tests_directory: '/tests',
|
||||
...overrides?.callInputs,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it('parses JUnit results and calls GraphQL mutation', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
expect(mockGraphqlClient.mutation).toHaveBeenCalledTimes(1);
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.workflowJobId).toBe('job-uuid-123');
|
||||
expect(variables.input.testCaseResults).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'home',
|
||||
path: '.maestro/home.yml',
|
||||
status: 'PASSED',
|
||||
errorMessage: null,
|
||||
duration: 10000,
|
||||
retryCount: 0,
|
||||
properties: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends tags and properties from JUnit XML', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="1" failures="0">',
|
||||
' <testcase id="home" name="home" classname="home" time="10.0" status="SUCCESS">',
|
||||
' <properties>',
|
||||
' <property name="testCaseId" value="TC-001"/>',
|
||||
' <property name="priority" value="high"/>',
|
||||
' <property name="tags" value="e2e"/>',
|
||||
' </properties>',
|
||||
' </testcase>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.testCaseResults[0].tags).toEqual(['e2e']);
|
||||
expect(variables.input.testCaseResults[0].properties).toEqual({
|
||||
testCaseId: 'TC-001',
|
||||
priority: 'high',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports failed flow with correct status and errorMessage', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_FAIL,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
|
||||
const [, variables] = (mockGraphqlClient.mutation as jest.Mock).mock.calls[0];
|
||||
expect(variables.input.testCaseResults[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 'FAILED',
|
||||
errorMessage: 'Tap failed',
|
||||
duration: 5000,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// robotAccessToken guard removed — Generic.JobZ requires robotAccessToken (z.string(),
|
||||
// not optional), so it's always present in workflow jobs. graphqlClient is already
|
||||
// initialized with the token in BuildContext constructor.
|
||||
|
||||
it('skips report when junit_report_directory is empty string (upstream step failed)', async () => {
|
||||
const step = createStep({
|
||||
callInputs: { junit_report_directory: '' },
|
||||
});
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips report when __WORKFLOW_JOB_ID env is not set', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
const step = createStep({ env: {} });
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips report when no JUnit files found', async () => {
|
||||
const step = createStep();
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not throw when GraphQL returns error', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
error: { message: 'Something went wrong' },
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
});
|
||||
|
||||
it('does not throw when mutation rejects', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': JUNIT_PASS,
|
||||
'/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
mockMutationFn.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await createStep().executeAsync();
|
||||
});
|
||||
|
||||
it('skips report when duplicate testcase names found', async () => {
|
||||
vol.fromJSON({
|
||||
'/junit/report.xml': [
|
||||
'<?xml version="1.0"?>',
|
||||
'<testsuites>',
|
||||
' <testsuite name="Test Suite" tests="2" failures="0">',
|
||||
' <testcase id="login" name="login" classname="login" time="10.0" status="SUCCESS"/>',
|
||||
' <testcase id="login" name="login" classname="login" time="5.0" status="SUCCESS"/>',
|
||||
' </testsuite>',
|
||||
'</testsuites>',
|
||||
].join('\n'),
|
||||
'/tests/2026-01-28_055409/ai-login.json': JSON.stringify({
|
||||
flow_name: 'login',
|
||||
flow_file_path: '/root/project/.maestro/login.yml',
|
||||
}),
|
||||
});
|
||||
|
||||
await createStep().executeAsync();
|
||||
expect(mockGraphqlClient.mutation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses default directories when inputs are not provided', async () => {
|
||||
vol.fromJSON({
|
||||
'/home/expo/.maestro/tests/report.xml': JUNIT_PASS,
|
||||
'/home/expo/.maestro/tests/2026-01-28_055409/ai-home.json': FLOW_AI,
|
||||
});
|
||||
|
||||
const ctx = { graphqlClient: mockGraphqlClient };
|
||||
const fn = createReportMaestroTestResultsFunction(ctx as any);
|
||||
const globalCtx = createGlobalContextMock({
|
||||
logger: createMockLogger(),
|
||||
projectTargetDirectory: '/root/project',
|
||||
staticContextContent: {
|
||||
expoApiServerURL: 'https://api.expo.test',
|
||||
job: { secrets: { robotAccessToken: 'test-token' } },
|
||||
},
|
||||
});
|
||||
globalCtx.updateEnv({ __WORKFLOW_JOB_ID: 'job-uuid-123', HOME: '/home/expo' });
|
||||
|
||||
mockMutationFn.mockResolvedValue({
|
||||
data: {
|
||||
workflowDeviceTestCaseResult: {
|
||||
createWorkflowDeviceTestCaseResults: [{ id: 'id-1' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const step = fn.createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {},
|
||||
});
|
||||
await step.executeAsync();
|
||||
expect(mockGraphqlClient.mutation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isClosedVersionTrainError } from '../uploadToAsc';
|
||||
|
||||
describe(isClosedVersionTrainError, () => {
|
||||
it('returns true when all errors are closed-version-train codes', () => {
|
||||
expect(
|
||||
isClosedVersionTrainError([{ code: '90062' }, { code: '90186' }, { code: '90062' }])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when any other error code is present', () => {
|
||||
expect(
|
||||
isClosedVersionTrainError([{ code: '90062' }, { code: '90186' }, { code: '12345' }])
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when there are no errors', () => {
|
||||
expect(isClosedVersionTrainError([])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,10 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
|
||||
id: 'test_reports_artifact_id',
|
||||
required: false,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'junit_report_directory',
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
fn: async (stepCtx, { inputs: _inputs, env, outputs }) => {
|
||||
// inputs come in form of { value: unknown }. Here we parse them into a typed and validated object.
|
||||
@@ -348,6 +352,10 @@ export function createInternalEasMaestroTestFunction(ctx: CustomBuildContext): B
|
||||
}
|
||||
}
|
||||
|
||||
if (output_format === 'junit') {
|
||||
outputs.junit_report_directory.set(maestroReportsDir);
|
||||
}
|
||||
|
||||
const generatedDeviceLogs = await fs.promises.readdir(deviceLogsDir);
|
||||
if (generatedDeviceLogs.length === 0) {
|
||||
stepCtx.logger.warn('No device logs were successfully collected.');
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface MaestroFlowResult {
|
||||
name: string;
|
||||
path: string;
|
||||
status: 'passed' | 'failed';
|
||||
errorMessage: string | null;
|
||||
duration: number; // milliseconds
|
||||
retryCount: number;
|
||||
tags: string[];
|
||||
properties: Record<string, string>;
|
||||
}
|
||||
|
||||
// Maestro's TestDebugReporter creates timestamped directories, e.g. "2024-06-15_143022"
|
||||
const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{6}$/;
|
||||
|
||||
export function extractFlowKey(filename: string, prefix: string): string | null {
|
||||
const match = filename.match(new RegExp(`^${prefix}-(.+)\\.json$`));
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
export interface JUnitTestCaseResult {
|
||||
name: string;
|
||||
status: 'passed' | 'failed';
|
||||
duration: number; // milliseconds
|
||||
errorMessage: string | null;
|
||||
tags: string[];
|
||||
properties: Record<string, string>;
|
||||
}
|
||||
|
||||
const xmlParser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_',
|
||||
// Ensure single-element arrays are always arrays
|
||||
isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
|
||||
});
|
||||
|
||||
export async function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]> {
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await fs.readdir(junitDirectory);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const xmlFiles = entries.filter(f => f.endsWith('.xml'));
|
||||
if (xmlFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: JUnitTestCaseResult[] = [];
|
||||
|
||||
for (const xmlFile of xmlFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(junitDirectory, xmlFile), 'utf-8');
|
||||
const parsed = xmlParser.parse(content);
|
||||
|
||||
const testsuites = parsed?.testsuites?.testsuite;
|
||||
if (!Array.isArray(testsuites)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const suite of testsuites) {
|
||||
const testcases = suite?.testcase;
|
||||
if (!Array.isArray(testcases)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const tc of testcases) {
|
||||
const name = tc['@_name'];
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const timeStr = tc['@_time'];
|
||||
const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
|
||||
const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
|
||||
|
||||
// Use @_status as primary indicator (more robust than checking <failure> presence)
|
||||
const status: 'passed' | 'failed' = tc['@_status'] === 'SUCCESS' ? 'passed' : 'failed';
|
||||
// Extract error message from <failure> or <error> elements
|
||||
const failureText =
|
||||
tc.failure != null
|
||||
? typeof tc.failure === 'string'
|
||||
? tc.failure
|
||||
: (tc.failure?.['#text'] ?? null)
|
||||
: null;
|
||||
const errorText =
|
||||
tc.error != null
|
||||
? typeof tc.error === 'string'
|
||||
? tc.error
|
||||
: (tc.error?.['#text'] ?? null)
|
||||
: null;
|
||||
const errorMessage: string | null = failureText ?? errorText ?? null;
|
||||
|
||||
// Extract properties
|
||||
const rawProperties: { '@_name': string; '@_value': string }[] =
|
||||
tc.properties?.property ?? [];
|
||||
const properties: Record<string, string> = {};
|
||||
|
||||
for (const prop of rawProperties) {
|
||||
const propName = prop['@_name'];
|
||||
const value = prop['@_value'];
|
||||
if (typeof propName !== 'string' || typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
properties[propName] = value;
|
||||
}
|
||||
|
||||
// Extract tags from "tags" property (Maestro 2.2.0+, comma-separated)
|
||||
const tagsValue = properties['tags'];
|
||||
const tags: string[] = tagsValue
|
||||
? tagsValue
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
delete properties['tags'];
|
||||
|
||||
results.push({ name, status, duration, errorMessage, tags, properties });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed XML files
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const FlowMetadataFileSchema = z.object({
|
||||
flow_name: z.string(),
|
||||
flow_file_path: z.string(),
|
||||
});
|
||||
|
||||
type FlowMetadata = z.output<typeof FlowMetadataFileSchema>;
|
||||
|
||||
/**
|
||||
* Parses an `ai-*.json` file produced by Maestro's TestDebugReporter.
|
||||
*
|
||||
* The file contains:
|
||||
* - `flow_name`: derived from the YAML `config.name` field if present, otherwise
|
||||
* the flow filename without extension.
|
||||
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt#L70
|
||||
* - `flow_file_path`: absolute path to the original flow YAML file.
|
||||
* - `outputs`: screenshot defect data (unused here).
|
||||
*
|
||||
* Filename format: `ai-(flowName).json` where `/` in flowName is replaced with `_`.
|
||||
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt#L67
|
||||
*/
|
||||
export async function parseFlowMetadata(filePath: string): Promise<FlowMetadata | null> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
return FlowMetadataFileSchema.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMaestroResults(
|
||||
junitDirectory: string,
|
||||
testsDirectory: string,
|
||||
projectRoot: string
|
||||
): Promise<MaestroFlowResult[]> {
|
||||
// 1. Parse JUnit XML files (primary source)
|
||||
const junitResults = await parseJUnitTestCases(junitDirectory);
|
||||
if (junitResults.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Parse ai-*.json from debug output for flow_file_path + retryCount
|
||||
const flowPathMap = new Map<string, string>(); // flowName → flowFilePath
|
||||
const flowOccurrences = new Map<string, number>(); // flowName → count
|
||||
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await fs.readdir(testsDirectory);
|
||||
} catch {
|
||||
entries = [];
|
||||
}
|
||||
|
||||
const timestampDirs = entries.filter(name => TIMESTAMP_DIR_PATTERN.test(name)).sort();
|
||||
|
||||
for (const dir of timestampDirs) {
|
||||
const dirPath = path.join(testsDirectory, dir);
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(dirPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const flowKey = extractFlowKey(file, 'ai');
|
||||
if (!flowKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata = await parseFlowMetadata(path.join(dirPath, file));
|
||||
if (!metadata) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track latest path (last timestamp dir wins)
|
||||
flowPathMap.set(metadata.flow_name, metadata.flow_file_path);
|
||||
|
||||
// Count occurrences for retryCount
|
||||
flowOccurrences.set(metadata.flow_name, (flowOccurrences.get(metadata.flow_name) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Merge: JUnit results + ai-*.json metadata
|
||||
const results: MaestroFlowResult[] = [];
|
||||
|
||||
for (const junit of junitResults) {
|
||||
const flowFilePath = flowPathMap.get(junit.name);
|
||||
const relativePath = flowFilePath
|
||||
? await relativizePathAsync(flowFilePath, projectRoot)
|
||||
: junit.name; // fallback: use flow name if ai-*.json not found
|
||||
|
||||
const occurrences = flowOccurrences.get(junit.name) ?? 0;
|
||||
const retryCount = Math.max(0, occurrences - 1);
|
||||
|
||||
results.push({
|
||||
name: junit.name,
|
||||
path: relativePath,
|
||||
status: junit.status,
|
||||
errorMessage: junit.errorMessage,
|
||||
duration: junit.duration,
|
||||
retryCount,
|
||||
tags: junit.tags,
|
||||
properties: junit.properties,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function relativizePathAsync(flowFilePath: string, projectRoot: string): Promise<string> {
|
||||
if (!path.isAbsolute(flowFilePath)) {
|
||||
return flowFilePath;
|
||||
}
|
||||
|
||||
// Resolve symlinks (e.g., /tmp -> /private/tmp on macOS) for consistent comparison
|
||||
let resolvedRoot = projectRoot;
|
||||
let resolvedFlow = flowFilePath;
|
||||
try {
|
||||
resolvedRoot = await fs.realpath(projectRoot);
|
||||
} catch {}
|
||||
try {
|
||||
resolvedFlow = await fs.realpath(flowFilePath);
|
||||
} catch {}
|
||||
|
||||
const relative = path.relative(resolvedRoot, resolvedFlow);
|
||||
if (relative.startsWith('..')) {
|
||||
return flowFilePath;
|
||||
}
|
||||
return relative;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepInput,
|
||||
BuildStepInputValueTypeName,
|
||||
BuildStepOutput,
|
||||
} from '@expo/steps';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
import { z } from 'zod';
|
||||
import bplistParser from 'bplist-parser';
|
||||
import plist from 'plist';
|
||||
|
||||
export type IpaInfo = {
|
||||
bundleIdentifier: string;
|
||||
bundleShortVersion: string;
|
||||
bundleVersion: string;
|
||||
};
|
||||
|
||||
const INFO_PLIST_PATH_REGEXP = /^Payload\/[^/]+\.app\/Info\.plist$/;
|
||||
|
||||
export function createReadIpaInfoBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'read_ipa_info',
|
||||
name: 'Read IPA Info',
|
||||
__metricsId: 'eas/read_ipa_info',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'ipa_path',
|
||||
required: true,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
],
|
||||
outputProviders: [
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'bundle_identifier',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'bundle_short_version',
|
||||
required: true,
|
||||
}),
|
||||
BuildStepOutput.createProvider({
|
||||
id: 'bundle_version',
|
||||
required: true,
|
||||
}),
|
||||
],
|
||||
fn: async (stepCtx, { inputs, outputs }) => {
|
||||
const ipaPathInput = z.string().parse(inputs.ipa_path.value);
|
||||
const ipaPath = path.resolve(stepCtx.workingDirectory, ipaPathInput);
|
||||
if (!(await fs.pathExists(ipaPath))) {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_FILE_NOT_FOUND',
|
||||
`IPA file not found: ${ipaPath}`
|
||||
);
|
||||
}
|
||||
|
||||
const ipaInfo = await readIpaInfoAsync(ipaPath);
|
||||
outputs.bundle_identifier.set(ipaInfo.bundleIdentifier);
|
||||
outputs.bundle_short_version.set(ipaInfo.bundleShortVersion);
|
||||
outputs.bundle_version.set(ipaInfo.bundleVersion);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo> {
|
||||
try {
|
||||
const infoPlistBuffer = await readInfoPlistBufferFromIpaAsync(ipaPath);
|
||||
const infoPlist = parseInfoPlistBuffer(infoPlistBuffer);
|
||||
|
||||
const bundleIdentifier = infoPlist.CFBundleIdentifier;
|
||||
if (typeof bundleIdentifier !== 'string') {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleIdentifier in Info.plist'
|
||||
);
|
||||
}
|
||||
|
||||
const bundleShortVersion = infoPlist.CFBundleShortVersionString;
|
||||
if (typeof bundleShortVersion !== 'string') {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleShortVersionString in Info.plist'
|
||||
);
|
||||
}
|
||||
|
||||
const bundleVersion = infoPlist.CFBundleVersion;
|
||||
if (typeof bundleVersion !== 'string') {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INVALID_INFO_PLIST',
|
||||
'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
bundleIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof UserFacingError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_FAILED',
|
||||
`Failed to read IPA info: ${(error as Error).message}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseInfoPlistBuffer(data: Buffer): Record<string, unknown> {
|
||||
const isBinaryPlist = data.subarray(0, 8).toString('ascii') === 'bplist00';
|
||||
if (isBinaryPlist) {
|
||||
const parsedBinaryPlists = bplistParser.parseBuffer(data);
|
||||
const parsedBinaryPlist = parsedBinaryPlists[0];
|
||||
if (!parsedBinaryPlist || typeof parsedBinaryPlist !== 'object') {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INVALID_BINARY_PLIST',
|
||||
'Invalid binary plist in IPA'
|
||||
);
|
||||
}
|
||||
return parsedBinaryPlist as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return plist.parse(data.toString('utf8')) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function readInfoPlistBufferFromIpaAsync(ipaPath: string): Promise<Buffer> {
|
||||
const zip = new StreamZip.async({ file: ipaPath });
|
||||
try {
|
||||
const entries = Object.values(await zip.entries());
|
||||
const infoPlistEntry = entries.find(entry => INFO_PLIST_PATH_REGEXP.test(entry.name));
|
||||
if (!infoPlistEntry) {
|
||||
throw new UserFacingError(
|
||||
'EAS_READ_IPA_INFO_INFO_PLIST_NOT_FOUND',
|
||||
`Failed to read IPA info: Could not find Info.plist in ${ipaPath}`
|
||||
);
|
||||
}
|
||||
return await zip.entryData(infoPlistEntry.name);
|
||||
} finally {
|
||||
await zip.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
|
||||
import { graphql } from 'gql.tada';
|
||||
|
||||
import { MaestroFlowResult, parseMaestroResults } from './maestroResultParser';
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
|
||||
const CREATE_MUTATION = graphql(`
|
||||
mutation CreateWorkflowDeviceTestCaseResults($input: CreateWorkflowDeviceTestCaseResultsInput!) {
|
||||
workflowDeviceTestCaseResult {
|
||||
createWorkflowDeviceTestCaseResults(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS: Record<string, 'PASSED' | 'FAILED' | undefined> = {
|
||||
passed: 'PASSED',
|
||||
failed: 'FAILED',
|
||||
} satisfies Record<MaestroFlowResult['status'], 'PASSED' | 'FAILED'>;
|
||||
|
||||
export function createReportMaestroTestResultsFunction(ctx: CustomBuildContext): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'report_maestro_test_results',
|
||||
name: 'Report Maestro Test Results',
|
||||
__metricsId: 'eas/report_maestro_test_results',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'junit_report_directory',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'tests_directory',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
||||
}),
|
||||
],
|
||||
fn: async (stepsCtx, { inputs }) => {
|
||||
const { logger } = stepsCtx;
|
||||
const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
|
||||
if (!workflowJobId) {
|
||||
logger.info('Not running in a workflow job, skipping test results report');
|
||||
return;
|
||||
}
|
||||
const junitDirectory = (inputs.junit_report_directory.value as string | undefined) ?? '';
|
||||
if (!junitDirectory) {
|
||||
logger.info('No JUnit directory provided, skipping test results report');
|
||||
return;
|
||||
}
|
||||
const testsDirectory = inputs.tests_directory.value as string;
|
||||
|
||||
try {
|
||||
const flowResults = await parseMaestroResults(
|
||||
junitDirectory,
|
||||
testsDirectory,
|
||||
stepsCtx.workingDirectory
|
||||
);
|
||||
if (flowResults.length === 0) {
|
||||
logger.info('No maestro test results found, skipping report');
|
||||
return;
|
||||
}
|
||||
|
||||
// Maestro allows overriding flow names via config, so different flow files can share
|
||||
// the same name. JUnit XML only contains names (not file paths), making it impossible
|
||||
// to map duplicates back to their original flow files. Skip and let the user fix it.
|
||||
const names = flowResults.map(r => r.name);
|
||||
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
logger.error(
|
||||
`Duplicate test case names found in JUnit output: ${[...new Set(duplicates)].join(
|
||||
', '
|
||||
)}. Skipping report. Ensure each Maestro flow has a unique name.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const testCaseResults = flowResults.flatMap(f => {
|
||||
const status = FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS[f.status];
|
||||
if (!status) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
status,
|
||||
errorMessage: f.errorMessage,
|
||||
duration: f.duration,
|
||||
retryCount: f.retryCount,
|
||||
tags: f.tags,
|
||||
properties: f.properties,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const result = await ctx.graphqlClient
|
||||
.mutation(CREATE_MUTATION, {
|
||||
input: {
|
||||
workflowJobId,
|
||||
testCaseResults,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
logger.error({ error: result.error }, 'GraphQL error creating test case results');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Reported ${testCaseResults.length} test case result(s).`);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Failed to create test case results');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildStepInput,
|
||||
@@ -12,6 +13,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AscApiClient, AscApiClientPostApi } from '../utils/ios/AscApiClient';
|
||||
import { AscApiUtils } from '../utils/ios/AscApiUtils';
|
||||
|
||||
export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
@@ -102,33 +104,17 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
const client = new AscApiClient({ token, logger: stepsCtx.logger });
|
||||
|
||||
stepsCtx.logger.info('Reading App information...');
|
||||
const appResponse = await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] },
|
||||
{ id: appleAppIdentifier }
|
||||
);
|
||||
const appResponse = await AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
|
||||
stepsCtx.logger.info(
|
||||
`Uploading Build to "${appResponse.data.attributes.name}" (${appResponse.data.attributes.bundleId})...`
|
||||
);
|
||||
|
||||
stepsCtx.logger.info('Creating Build Upload...');
|
||||
const buildUploadResponse = await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: bundleShortVersion,
|
||||
cfBundleVersion: bundleVersion,
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: appleAppIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const buildUploadResponse = await AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
});
|
||||
|
||||
const buildUploadId = buildUploadResponse.data.id;
|
||||
@@ -234,7 +220,10 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
|
||||
stepsCtx.logger.info('Checking build upload status...');
|
||||
const waitingForBuildStartedAt = Date.now();
|
||||
while (Date.now() - waitingForBuildStartedAt < 10 * 60 * 1000 /* 10 minutes */) {
|
||||
const waitingLogIntervalMs = 10 * 1000;
|
||||
let lastWaitLogTime = 0;
|
||||
let lastWaitLogState: string | null = null;
|
||||
while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) {
|
||||
const {
|
||||
data: {
|
||||
attributes: { state },
|
||||
@@ -246,7 +235,14 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
);
|
||||
|
||||
if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') {
|
||||
stepsCtx.logger.info(`Waiting for build upload to complete... (status = ${state.state})`);
|
||||
const now = Date.now();
|
||||
if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) {
|
||||
stepsCtx.logger.info(
|
||||
`Waiting for build upload to complete... (status = ${state.state})`
|
||||
);
|
||||
lastWaitLogTime = now;
|
||||
lastWaitLogState = state.state;
|
||||
}
|
||||
await setTimeout(2000);
|
||||
continue;
|
||||
}
|
||||
@@ -265,6 +261,13 @@ export function createUploadToAscBuildFunction(): BuildFunction {
|
||||
}
|
||||
|
||||
if (state.state === 'FAILED') {
|
||||
if (isClosedVersionTrainError(errors)) {
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN',
|
||||
`Build upload was rejected by App Store Connect because the ${bundleShortVersion} version train is closed. ` +
|
||||
'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version and submit again.'
|
||||
);
|
||||
}
|
||||
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
|
||||
} else if (state.state === 'COMPLETE') {
|
||||
stepsCtx.logger.info(`Build upload (ID: ${buildUploadId}) complete!`);
|
||||
@@ -279,6 +282,12 @@ function itemizeMessages(messages: { description: string; code: string }[]): str
|
||||
return `- ${messages.map(m => `${m.description} (${m.code})`).join('\n- ')}`;
|
||||
}
|
||||
|
||||
export function isClosedVersionTrainError(messages: { code: string }[]): boolean {
|
||||
return (
|
||||
messages.length > 0 && messages.every(message => ['90062', '90186'].includes(message.code))
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadChunksAsync({
|
||||
uploadOperations,
|
||||
ipaPath,
|
||||
|
||||
@@ -11,7 +11,43 @@ type ApiSchema = {
|
||||
};
|
||||
};
|
||||
|
||||
const AscErrorResponseSchema = z.object({
|
||||
errors: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
detail: z.string().optional(),
|
||||
source: z.unknown().optional(),
|
||||
})
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const GetApi = {
|
||||
'/v1/apps': {
|
||||
path: z.object({}),
|
||||
request: z.object({
|
||||
'fields[apps]': z.array(z.enum(['bundleId', 'name'])).refine(opts => {
|
||||
return opts.includes('bundleId') && opts.includes('name');
|
||||
}),
|
||||
limit: z.number().int().min(1).max(200).optional(),
|
||||
}),
|
||||
response: z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
type: z.literal('apps'),
|
||||
id: z.string(),
|
||||
attributes: z.object({
|
||||
bundleId: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
'/v1/apps/:id': {
|
||||
path: z.object({
|
||||
id: z.string(),
|
||||
@@ -222,6 +258,13 @@ const PatchApi = {
|
||||
},
|
||||
} satisfies ApiSchema;
|
||||
|
||||
export type AscApiClientGetApi = {
|
||||
[Path in keyof typeof GetApi]: {
|
||||
request: z.input<(typeof GetApi)[Path]['request']>;
|
||||
response: z.output<(typeof GetApi)[Path]['response']>;
|
||||
};
|
||||
};
|
||||
|
||||
export type AscApiClientPostApi = {
|
||||
[Path in keyof typeof PostApi]: {
|
||||
request: z.input<(typeof PostApi)[Path]['request']>;
|
||||
@@ -236,6 +279,17 @@ export type AscApiClientPatchApi = {
|
||||
};
|
||||
};
|
||||
|
||||
export class AscApiRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
public readonly responseJson: z.output<typeof AscErrorResponseSchema>,
|
||||
options?: { cause?: unknown }
|
||||
) {
|
||||
super(message, { cause: options?.cause });
|
||||
}
|
||||
}
|
||||
|
||||
export class AscApiClient {
|
||||
private readonly baseUrl = 'https://api.appstoreconnect.apple.com';
|
||||
private readonly token: string;
|
||||
@@ -346,12 +400,31 @@ export class AscApiClient {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const parsedAscErrorResponse = await asyncResult(
|
||||
(async () => AscErrorResponseSchema.parse(JSON.parse(text)))()
|
||||
);
|
||||
if (parsedAscErrorResponse.ok) {
|
||||
throw new AscApiRequestError(
|
||||
`Unexpected response (${response.status}) from App Store Connect: ${text}`,
|
||||
response.status,
|
||||
parsedAscErrorResponse.value,
|
||||
{ cause: response }
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected response (${response.status}) from App Store Connect: ${text}`, {
|
||||
cause: response,
|
||||
});
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const text = await response.text();
|
||||
const parsedJson = await asyncResult((async () => JSON.parse(text))());
|
||||
if (!parsedJson.ok) {
|
||||
throw new Error(
|
||||
`Malformed JSON response from App Store Connect (${response.status}): ${text}`
|
||||
);
|
||||
}
|
||||
const json = parsedJson.value;
|
||||
|
||||
this.logger?.debug(`Response from App Store Connect: ${JSON.stringify(json, null, 2)}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
|
||||
import {
|
||||
AscApiClient,
|
||||
AscApiClientGetApi,
|
||||
AscApiClientPostApi,
|
||||
AscApiRequestError,
|
||||
} from './AscApiClient';
|
||||
|
||||
export namespace AscApiUtils {
|
||||
export async function getAppInfoAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
}: {
|
||||
client: Pick<AscApiClient, 'getAsync'>;
|
||||
appleAppIdentifier: string;
|
||||
}): Promise<AscApiClientGetApi['/v1/apps/:id']['response']> {
|
||||
try {
|
||||
return await client.getAsync(
|
||||
'/v1/apps/:id',
|
||||
{ 'fields[apps]': ['bundleId', 'name'] },
|
||||
{ id: appleAppIdentifier }
|
||||
);
|
||||
} catch (error) {
|
||||
const notFoundErrors =
|
||||
error instanceof AscApiRequestError && error.status === 404
|
||||
? error.responseJson.errors
|
||||
: [];
|
||||
const isAppNotFoundError =
|
||||
notFoundErrors.length > 0 && notFoundErrors.every(item => item.code === 'NOT_FOUND');
|
||||
if (!isAppNotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let visibleAppsSummary: string | null = null;
|
||||
try {
|
||||
visibleAppsSummary = await getVisibleAppsSummaryAsync(client);
|
||||
} catch {
|
||||
// Don't hide the original NOT_FOUND error with a secondary lookup failure.
|
||||
throw error;
|
||||
}
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_APP_NOT_FOUND',
|
||||
`App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
|
||||
'Verify the configured application identifier and that the App Store Connect API key has access to the application in the correct App Store Connect account.' +
|
||||
(visibleAppsSummary
|
||||
? `\n\nExample applications visible to this API key:\n${visibleAppsSummary}`
|
||||
: ''),
|
||||
{
|
||||
cause: error,
|
||||
docsUrl: 'https://expo.fyi/asc-app-id',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier,
|
||||
bundleShortVersion,
|
||||
bundleVersion,
|
||||
}: {
|
||||
client: Pick<AscApiClient, 'postAsync'>;
|
||||
appleAppIdentifier: string;
|
||||
bundleShortVersion: string;
|
||||
bundleVersion: string;
|
||||
}): Promise<AscApiClientPostApi['/v1/buildUploads']['response']> {
|
||||
try {
|
||||
return await client.postAsync('/v1/buildUploads', {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
attributes: {
|
||||
platform: 'IOS',
|
||||
cfBundleShortVersionString: bundleShortVersion,
|
||||
cfBundleVersion: bundleVersion,
|
||||
},
|
||||
relationships: {
|
||||
app: {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: appleAppIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errors =
|
||||
error instanceof AscApiRequestError && error.status === 409
|
||||
? error.responseJson.errors
|
||||
: [];
|
||||
const isDuplicateVersionError =
|
||||
errors.length > 0 &&
|
||||
errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
|
||||
|
||||
if (isDuplicateVersionError) {
|
||||
throw new UserFacingError(
|
||||
'EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE',
|
||||
`Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
|
||||
'App Store Connect requires unique build numbers within each app version (version train). ' +
|
||||
'Increment it by setting ios.buildNumber in app.json, or set "autoIncrement": true in eas.json (recommended). Then rebuild and resubmit.',
|
||||
{
|
||||
cause: error,
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
||||
}
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getVisibleAppsSummaryAsync(
|
||||
client: Pick<AscApiClient, 'getAsync'>
|
||||
): Promise<string | null> {
|
||||
const appsResponse = await client.getAsync('/v1/apps', {
|
||||
'fields[apps]': ['bundleId', 'name'],
|
||||
limit: 10,
|
||||
});
|
||||
if (appsResponse.data.length === 0) {
|
||||
return ' (none)';
|
||||
}
|
||||
return appsResponse.data
|
||||
.map(app => `- ${app.attributes.name} (${app.attributes.bundleId}) (ID: ${app.id})`)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import { AscApiClient } from '../AscApiClient';
|
||||
import { AscApiClient, AscApiRequestError } from '../AscApiClient';
|
||||
|
||||
// nock needs real fetch implementation
|
||||
jest.unmock('node-fetch');
|
||||
@@ -280,4 +280,54 @@ describe(AscApiClient, () => {
|
||||
});
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws AscApiRequestError for structured ASC error payload', async () => {
|
||||
const appId = '1491144534';
|
||||
const responseFixture = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
title:
|
||||
'The provided entity includes an attribute with a value that has already been used',
|
||||
detail: 'The bundle version must be higher than the previously uploaded version.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(409, responseFixture);
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.toBeInstanceOf(AscApiRequestError);
|
||||
});
|
||||
|
||||
it('throws regular Error for non-structured ASC error payload', async () => {
|
||||
const appId = '1491144534';
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(503, 'Service unavailable');
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.not.toBeInstanceOf(AscApiRequestError);
|
||||
});
|
||||
|
||||
it('throws controlled error when success response body is not valid JSON', async () => {
|
||||
const appId = '1491144534';
|
||||
|
||||
nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/apps/${appId}`)
|
||||
.query({ 'fields[apps]': 'bundleId,name' })
|
||||
.reply(200, 'not-json');
|
||||
|
||||
await expect(
|
||||
client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appId })
|
||||
).rejects.toThrow('Malformed JSON response from App Store Connect (200): not-json');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { AscApiRequestError } from '../AscApiClient';
|
||||
import { AscApiUtils } from '../AscApiUtils';
|
||||
|
||||
describe('AscApiUtils', () => {
|
||||
describe('getAppInfoAsync', () => {
|
||||
it('returns app info when lookup succeeds', async () => {
|
||||
const response = {
|
||||
data: {
|
||||
type: 'apps',
|
||||
id: '1491144534',
|
||||
attributes: {
|
||||
name: 'Example App',
|
||||
bundleId: 'com.example.app',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const client = {
|
||||
getAsync: jest.fn().mockResolvedValue(response),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1491144534' })
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('throws UserFacingError with visible apps when app id is not found', async () => {
|
||||
const notFoundPayload = {
|
||||
errors: [
|
||||
{
|
||||
status: '404',
|
||||
code: 'NOT_FOUND',
|
||||
detail: "There is no resource of type 'apps' with id '1234567890'",
|
||||
},
|
||||
],
|
||||
};
|
||||
const notFoundError = new AscApiRequestError(
|
||||
'Unexpected response (404) from App Store Connect',
|
||||
404,
|
||||
notFoundPayload
|
||||
);
|
||||
const client = {
|
||||
getAsync: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(notFoundError)
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
type: 'apps',
|
||||
id: '1111111111',
|
||||
attributes: { name: 'Visible App', bundleId: 'com.visible.app' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1234567890' })
|
||||
).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorCode: 'EAS_UPLOAD_TO_ASC_APP_NOT_FOUND',
|
||||
docsUrl: 'https://expo.fyi/asc-app-id',
|
||||
message: expect.stringContaining(
|
||||
'App Store Connect app for application identifier 1234567890 was not found'
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows original not-found error when app-list lookup fails', async () => {
|
||||
const notFoundPayload = {
|
||||
errors: [
|
||||
{
|
||||
status: '404',
|
||||
code: 'NOT_FOUND',
|
||||
},
|
||||
],
|
||||
};
|
||||
const notFoundError = new AscApiRequestError(
|
||||
'Unexpected response (404) from App Store Connect',
|
||||
404,
|
||||
notFoundPayload
|
||||
);
|
||||
|
||||
const listingError = new Error('listing failed');
|
||||
const client = {
|
||||
getAsync: jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(notFoundError)
|
||||
.mockRejectedValueOnce(listingError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier: '1234567890' })
|
||||
).rejects.toBe(notFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBuildUploadAsync', () => {
|
||||
it('throws UserFacingError when ASC duplicate version error is returned', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
detail: 'The bundle version must be higher than the previously uploaded version.',
|
||||
},
|
||||
],
|
||||
};
|
||||
const duplicateError = new AscApiRequestError(
|
||||
'Unexpected response (409) from App Store Connect',
|
||||
409,
|
||||
payload
|
||||
);
|
||||
|
||||
const client = {
|
||||
postAsync: jest.fn().mockRejectedValue(duplicateError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
errorCode: 'EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE',
|
||||
message: expect.stringContaining('Increment Build Number'),
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns response when upload initialization succeeds', async () => {
|
||||
const response = {
|
||||
data: {
|
||||
type: 'buildUploads',
|
||||
id: 'fdf9c476-aaa4-4ead-b91c-6e3cc3a47805',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const client = {
|
||||
postAsync: jest.fn().mockResolvedValue(response),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it('rethrows when error payload includes mixed error codes', async () => {
|
||||
const payload = {
|
||||
errors: [
|
||||
{
|
||||
status: '409',
|
||||
code: 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE',
|
||||
},
|
||||
{
|
||||
status: '409',
|
||||
code: 'SOME_OTHER_ERROR',
|
||||
},
|
||||
],
|
||||
};
|
||||
const mixedError = new AscApiRequestError(
|
||||
'Unexpected response (409) from App Store Connect',
|
||||
409,
|
||||
payload
|
||||
);
|
||||
const client = {
|
||||
postAsync: jest.fn().mockRejectedValue(mixedError),
|
||||
};
|
||||
|
||||
await expect(
|
||||
AscApiUtils.createBuildUploadAsync({
|
||||
client,
|
||||
appleAppIdentifier: '1491144534',
|
||||
bundleShortVersion: '1.2.3',
|
||||
bundleVersion: '42',
|
||||
})
|
||||
).rejects.toBe(mixedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "18.0.1",
|
||||
"version": "18.0.3",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -15,9 +15,7 @@
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/create-eas-build-function"
|
||||
},
|
||||
"bin": {
|
||||
"create-eas-build-function": "./build/index.js"
|
||||
},
|
||||
"bin": "./build/index.js",
|
||||
"files": [
|
||||
"build",
|
||||
"templates"
|
||||
@@ -34,10 +32,10 @@
|
||||
"build:dev": "ncc build ./src/index.ts -o build/",
|
||||
"build": "ncc build ./src/index.ts -o build/ --minify --no-cache --no-source-map-register",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rimraf ./build/ *.tsbuildinfo"
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "18.0.1"
|
||||
"@expo/steps": "18.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
@@ -49,7 +47,9 @@
|
||||
"chalk": "4.1.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"ora": "^6.3.1",
|
||||
"oxlint": "1.43.0",
|
||||
"prompts": "^2.4.2",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.4",
|
||||
"update-check": "^1.5.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-build-cache-provider",
|
||||
"version": "18.0.1",
|
||||
"version": "18.0.2",
|
||||
"description": "A build cache provider plugin for the Expo CLI",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
@@ -48,6 +48,7 @@
|
||||
"@types/babel__code-frame": "7.0.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"jest": "29.7.0",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "5.5.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "18.0.1",
|
||||
"version": "18.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -340,6 +340,60 @@ describe('Android.JobSchema', () => {
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can set hooks with arbitrary names', () => {
|
||||
const job = {
|
||||
mode: BuildMode.BUILD,
|
||||
type: Workflow.GENERIC,
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://expo.dev/builds/123',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
hooks: {
|
||||
before_install_node_modules: [
|
||||
{
|
||||
id: 'before-install',
|
||||
run: 'echo before install',
|
||||
shell: 'sh',
|
||||
},
|
||||
],
|
||||
my_custom_hook: [],
|
||||
},
|
||||
secrets,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
};
|
||||
const { value, error } = Android.JobSchema.validate(job, joiOptions);
|
||||
expect(value).toMatchObject(job);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can set hooks in custom mode', () => {
|
||||
const customBuildJob = {
|
||||
mode: BuildMode.CUSTOM,
|
||||
type: Workflow.UNKNOWN,
|
||||
platform: Platform.ANDROID,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://expo.dev/builds/123',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
customBuildConfig: {
|
||||
path: 'production.android.yml',
|
||||
},
|
||||
hooks: {
|
||||
before_install_node_modules: [],
|
||||
},
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
};
|
||||
|
||||
const { value, error } = Android.JobSchema.validate(customBuildJob, joiOptions);
|
||||
expect(value).toMatchObject(customBuildJob);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can set github trigger options', () => {
|
||||
const job = {
|
||||
mode: BuildMode.BUILD,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { UserFacingError } from '../errors';
|
||||
|
||||
describe(UserFacingError, () => {
|
||||
it('supports docsUrl in options', () => {
|
||||
const error = new UserFacingError('ERR_CODE', 'message', {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
});
|
||||
|
||||
expect(error.docsUrl).toBe('https://docs.example.dev');
|
||||
expect(error.cause).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports cause in options', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new UserFacingError('ERR_CODE', 'message', { cause });
|
||||
|
||||
expect(error.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it('supports docsUrl and cause in options', () => {
|
||||
const cause = new Error('root cause');
|
||||
const error = new UserFacingError('ERR_CODE', 'message', {
|
||||
docsUrl: 'https://docs.example.dev',
|
||||
cause,
|
||||
});
|
||||
|
||||
expect(error.docsUrl).toBe('https://docs.example.dev');
|
||||
expect(error.cause).toBe(cause);
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,107 @@ describe('Generic.JobZ', () => {
|
||||
expect(Generic.JobZ.parse(job)).toEqual(job);
|
||||
});
|
||||
|
||||
it('accepts hooks with arbitrary names', () => {
|
||||
const job: Generic.Job = {
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://github.com/expo/expo.git',
|
||||
gitCommitHash: '1234567890',
|
||||
gitRef: null,
|
||||
},
|
||||
hooks: {
|
||||
before_install_node_modules: [
|
||||
{
|
||||
id: 'before-install',
|
||||
run: 'echo before install',
|
||||
shell: 'sh',
|
||||
},
|
||||
],
|
||||
my_custom_hook: [],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
run: 'echo Hello, world!',
|
||||
shell: 'sh',
|
||||
},
|
||||
],
|
||||
secrets: {
|
||||
robotAccessToken: 'token',
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'secret-name',
|
||||
value: 'secret-value',
|
||||
type: EnvironmentSecretType.STRING,
|
||||
},
|
||||
],
|
||||
},
|
||||
expoDevUrl: 'https://expo.dev/accounts/name/builds/id',
|
||||
builderEnvironment: {
|
||||
image: 'macos-sonoma-14.5-xcode-15.4',
|
||||
node: '20.15.1',
|
||||
corepack: false,
|
||||
env: {
|
||||
KEY1: 'value1',
|
||||
},
|
||||
},
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
appId: randomUUID(),
|
||||
initiatingUserId: randomUUID(),
|
||||
};
|
||||
expect(Generic.JobZ.parse(job)).toEqual(job);
|
||||
});
|
||||
|
||||
it('errors when hook step definition is invalid', () => {
|
||||
const job = {
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://github.com/expo/expo.git',
|
||||
gitCommitHash: '1234567890',
|
||||
gitRef: null,
|
||||
},
|
||||
hooks: {
|
||||
before_install_node_modules: [
|
||||
{
|
||||
id: 'missing-run-or-uses',
|
||||
},
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
run: 'echo Hello, world!',
|
||||
shell: 'sh',
|
||||
},
|
||||
],
|
||||
secrets: {
|
||||
robotAccessToken: 'token',
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'secret-name',
|
||||
value: 'secret-value',
|
||||
type: EnvironmentSecretType.STRING,
|
||||
},
|
||||
],
|
||||
},
|
||||
expoDevUrl: 'https://expo.dev/accounts/name/builds/id',
|
||||
builderEnvironment: {
|
||||
image: 'macos-sonoma-14.5-xcode-15.4',
|
||||
node: '20.15.1',
|
||||
corepack: false,
|
||||
env: {
|
||||
KEY1: 'value1',
|
||||
},
|
||||
},
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
appId: randomUUID(),
|
||||
initiatingUserId: randomUUID(),
|
||||
};
|
||||
expect(() => Generic.JobZ.parse(job)).toThrow(ZodError);
|
||||
});
|
||||
|
||||
it('errors when steps are not provided', () => {
|
||||
const job: Omit<Generic.Job, 'customBuildConfig' | 'steps'> = {
|
||||
projectArchive: {
|
||||
|
||||
@@ -84,6 +84,66 @@ describe('Ios.JobSchema', () => {
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('valid generic job with hooks', () => {
|
||||
const genericJob = {
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
type: Workflow.GENERIC,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'http://localhost:3000',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
hooks: {
|
||||
before_install_node_modules: [
|
||||
{
|
||||
id: 'before-install',
|
||||
run: 'echo before install',
|
||||
shell: 'sh',
|
||||
},
|
||||
],
|
||||
my_custom_hook: [],
|
||||
},
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
};
|
||||
|
||||
const { value, error } = Ios.JobSchema.validate(genericJob, joiOptions);
|
||||
expect(value).toMatchObject(genericJob);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('valid resign job with hooks', () => {
|
||||
const genericJob = {
|
||||
mode: BuildMode.RESIGN,
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
type: Workflow.UNKNOWN,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.NONE,
|
||||
},
|
||||
resign: {
|
||||
applicationArchiveSource: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'http://localhost:3000/a.ipa',
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
before_install_node_modules: [],
|
||||
},
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
};
|
||||
|
||||
const { value, error } = Ios.JobSchema.validate(genericJob, joiOptions);
|
||||
expect(value).toMatchObject(genericJob);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('valid custom build job with metadataLocation', () => {
|
||||
const customBuildJob = {
|
||||
mode: BuildMode.CUSTOM,
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
EnvSchema,
|
||||
EnvironmentSecret,
|
||||
EnvironmentSecretsSchema,
|
||||
Hooks,
|
||||
HooksSchema,
|
||||
Platform,
|
||||
StaticWorkflowInterpolationContext,
|
||||
StaticWorkflowInterpolationContextZ,
|
||||
@@ -106,6 +108,7 @@ export interface Job {
|
||||
customBuildConfig?: {
|
||||
path: string;
|
||||
};
|
||||
hooks?: Hooks;
|
||||
steps?: Step[];
|
||||
outputs?: Record<string, string>;
|
||||
|
||||
@@ -172,6 +175,7 @@ export const JobSchema = Joi.object({
|
||||
|
||||
buildType: Joi.string().valid(...Object.values(BuildType)),
|
||||
username: Joi.string(),
|
||||
hooks: HooksSchema,
|
||||
|
||||
experimental: Joi.object({
|
||||
prebuildCommand: Joi.string(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import Joi from 'joi';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BuildPhase, BuildPhaseResult } from './logs';
|
||||
import { validateSteps } from './step';
|
||||
import { Step, StepZ, validateSteps } from './step';
|
||||
|
||||
export enum BuildMode {
|
||||
BUILD = 'build',
|
||||
@@ -142,6 +142,21 @@ export const EnvironmentSecretZ = z.object({
|
||||
type: z.nativeEnum(EnvironmentSecretType),
|
||||
});
|
||||
|
||||
export type Hooks = Record<string, Step[]>;
|
||||
export const HooksSchema = Joi.object().pattern(
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.items(Joi.any())
|
||||
.required()
|
||||
.custom(steps => {
|
||||
if (steps.length === 0) {
|
||||
return steps;
|
||||
}
|
||||
return validateSteps(steps);
|
||||
}, 'steps validation')
|
||||
);
|
||||
export const HooksZ = z.record(z.string(), z.array(StepZ));
|
||||
|
||||
export interface Cache {
|
||||
disabled: boolean;
|
||||
clear: boolean;
|
||||
|
||||
@@ -54,12 +54,15 @@ export class BuildError extends Error {
|
||||
}
|
||||
|
||||
export class UserFacingError extends Error {
|
||||
public docsUrl?: string;
|
||||
|
||||
constructor(
|
||||
public errorCode: string,
|
||||
public message: string,
|
||||
public docsUrl?: string
|
||||
options?: { docsUrl?: string; cause?: unknown }
|
||||
) {
|
||||
super(message);
|
||||
super(message, { cause: options?.cause });
|
||||
this.docsUrl = options?.docsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ArchiveSourceSchemaZ,
|
||||
BuildTrigger,
|
||||
EnvironmentSecretZ,
|
||||
HooksZ,
|
||||
StaticWorkflowInterpolationContextZ,
|
||||
} from './common';
|
||||
import { StepZ } from './step';
|
||||
@@ -45,6 +46,7 @@ export namespace Generic {
|
||||
initiatingUserId: z.string(),
|
||||
appId: z.string(),
|
||||
|
||||
hooks: HooksZ.optional(),
|
||||
steps: z.array(StepZ).min(1),
|
||||
outputs: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
Env,
|
||||
EnvironmentSecret,
|
||||
EnvironmentSecretType,
|
||||
Hooks,
|
||||
Workflow,
|
||||
Platform,
|
||||
Cache,
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
EnvSchema,
|
||||
EnvironmentSecret,
|
||||
EnvironmentSecretsSchema,
|
||||
Hooks,
|
||||
HooksSchema,
|
||||
Platform,
|
||||
StaticWorkflowInterpolationContext,
|
||||
StaticWorkflowInterpolationContextZ,
|
||||
@@ -120,6 +122,7 @@ export interface Job {
|
||||
customBuildConfig?: {
|
||||
path: string;
|
||||
};
|
||||
hooks?: Hooks;
|
||||
steps?: Step[];
|
||||
outputs?: Record<string, string>;
|
||||
|
||||
@@ -206,6 +209,7 @@ export const JobSchema = Joi.object({
|
||||
applicationArchivePath: Joi.string(),
|
||||
|
||||
username: Joi.string(),
|
||||
hooks: HooksSchema,
|
||||
|
||||
experimental: Joi.object({
|
||||
prebuildCommand: Joi.string(),
|
||||
|
||||
+91
-90
@@ -178,7 +178,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -195,7 +195,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -217,7 +217,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -234,7 +234,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -248,7 +248,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -300,7 +300,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -321,7 +321,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -341,7 +341,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -361,7 +361,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -384,7 +384,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -425,7 +425,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -444,7 +444,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -461,7 +461,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -480,7 +480,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -500,7 +500,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -521,7 +521,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -556,7 +556,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -608,7 +608,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -637,7 +637,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -663,7 +663,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -717,7 +717,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -736,7 +736,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -755,7 +755,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -772,7 +772,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -793,7 +793,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -814,7 +814,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -836,7 +836,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -856,7 +856,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -878,7 +878,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -900,7 +900,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -933,7 +933,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -956,7 +956,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -977,7 +977,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -994,7 +994,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1012,7 +1012,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1040,7 +1040,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1065,7 +1065,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1086,7 +1086,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1107,7 +1107,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1144,7 +1144,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1164,7 +1164,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1185,7 +1185,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1206,7 +1206,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1220,7 +1220,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1234,7 +1234,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1264,7 +1264,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1289,7 +1289,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1311,7 +1311,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1336,7 +1336,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1362,7 +1362,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1384,7 +1384,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1406,7 +1406,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1438,7 +1438,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1482,7 +1482,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1512,7 +1512,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1621,7 +1621,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1638,7 +1638,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1655,7 +1655,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1706,7 +1706,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1720,7 +1720,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1743,7 +1743,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -1766,7 +1766,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1784,7 +1784,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1819,7 +1819,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1841,7 +1841,8 @@ FLAGS
|
||||
--emit-metadata Emit "eas-update-metadata.json" in the bundle folder with detailed information about
|
||||
the generated updates
|
||||
--environment=<value> Environment to use for the server-side defined EAS environment variables during
|
||||
command execution, e.g. "production", "preview", "development"
|
||||
command execution, e.g. "production", "preview", "development". Required for
|
||||
projects using Expo SDK 55 or greater.
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
@@ -1859,7 +1860,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1872,14 +1873,14 @@ USAGE
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all] Platform to configure
|
||||
--environment=<value> Environment to use for the server-side defined EAS environment variables during
|
||||
command execution, e.g. "production", "preview", "development"
|
||||
command execution, e.g. "production", "preview", "development".
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1900,7 +1901,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1924,7 +1925,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1949,7 +1950,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1985,7 +1986,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2012,7 +2013,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2040,7 +2041,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2061,7 +2062,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2081,7 +2082,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2102,7 +2103,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2123,7 +2124,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2143,7 +2144,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2161,7 +2162,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2183,7 +2184,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2200,7 +2201,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2320,7 +2321,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2340,7 +2341,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2364,7 +2365,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run [FILE]`
|
||||
|
||||
@@ -2404,7 +2405,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2429,7 +2430,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2457,7 +2458,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2477,7 +2478,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2499,5 +2500,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.0.1/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v18.1.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "18.0.1",
|
||||
"version": "18.1.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -36,7 +36,7 @@
|
||||
"typecheck": "tsc",
|
||||
"typecheck-for-build": "tsc --project tsconfig.build.json",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"version": "yarn run -T 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 *.tsbuildinfo",
|
||||
@@ -47,8 +47,8 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/eas-json": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/eas-json": "18.0.2",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "18.0.1",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.0.1",
|
||||
"@expo/steps": "18.0.2",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
@@ -158,6 +158,7 @@
|
||||
"axios": "1.11.0",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.20.0",
|
||||
"jest": "29.7.0",
|
||||
"memfs": "3.4.13",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.4.0",
|
||||
|
||||
@@ -2,6 +2,6 @@ const spawn = require('@expo/spawn-async');
|
||||
|
||||
(async () => {
|
||||
if (!process.env.CLI_SIZE_CHECK) {
|
||||
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
|
||||
await spawn('yarn', ['run', '-T', 'oclif', 'manifest'], { stdio: 'inherit' });
|
||||
}
|
||||
})();
|
||||
@@ -27,7 +27,7 @@ if cmp -s graphql.schema.json graphql.schema.json.bak; then
|
||||
else
|
||||
echo "GraphQL schema has changed on the server."
|
||||
echo "Building eas-cli and its dependencies"
|
||||
yarn lerna run build --scope eas-cli --include-dependencies
|
||||
yarn run -T lerna run build --scope eas-cli --include-dependencies
|
||||
# If there are no errors, then the schema is backwards compatible
|
||||
exit 0
|
||||
fi
|
||||
@@ -66,7 +66,7 @@ describe(EasCommand.name, () => {
|
||||
|
||||
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync');
|
||||
expect(sessionManagerSpy).toBeCalledTimes(1);
|
||||
}, 30_000);
|
||||
}, 60_000);
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
|
||||
@@ -67,7 +67,16 @@ export const EasJsonOnlyFlag = {
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development".',
|
||||
required: false,
|
||||
default: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentRequiredFlag = {
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development". Required for projects using Expo SDK 55 or greater.',
|
||||
required: false,
|
||||
default: undefined,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { parsedYamlFromWorkflowContents } from '../parse';
|
||||
|
||||
describe('parsedYamlFromWorkflowContents', () => {
|
||||
it('parses YAML workflow contents', () => {
|
||||
const yamlConfig = `
|
||||
jobs:
|
||||
test:
|
||||
type: custom
|
||||
`;
|
||||
|
||||
expect(parsedYamlFromWorkflowContents({ yamlConfig })).toEqual({
|
||||
jobs: {
|
||||
test: {
|
||||
type: 'custom',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('supports shared env merge keys across multiple jobs', () => {
|
||||
const yamlConfig = `
|
||||
shared_env: &shared_env
|
||||
EXPO_TOKEN: token
|
||||
CI: "1"
|
||||
jobs:
|
||||
android:
|
||||
type: custom
|
||||
env:
|
||||
<<: *shared_env
|
||||
PLATFORM: android
|
||||
ios:
|
||||
type: custom
|
||||
env:
|
||||
<<: *shared_env
|
||||
PLATFORM: ios
|
||||
`;
|
||||
|
||||
expect(parsedYamlFromWorkflowContents({ yamlConfig })).toEqual({
|
||||
shared_env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
},
|
||||
jobs: {
|
||||
android: {
|
||||
type: 'custom',
|
||||
env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
PLATFORM: 'android',
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
type: 'custom',
|
||||
env: {
|
||||
EXPO_TOKEN: 'token',
|
||||
CI: '1',
|
||||
PLATFORM: 'ios',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
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';
|
||||
import { parsedYamlFromWorkflowContents } from './parse';
|
||||
|
||||
const inputExtraProperties = {
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
@@ -83,7 +83,7 @@ export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
const parsed = parsedYamlFromWorkflowContents({ yamlConfig });
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
export function parsedYamlFromWorkflowContents(workflowFileContents: { yamlConfig: string }): any {
|
||||
return YAML.parse(workflowFileContents.yamlConfig, {
|
||||
// Keep in sync with backend parser options.
|
||||
merge: true,
|
||||
maxAliasCount: 50,
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import Log from '../../log';
|
||||
import { createValidator, getReadableErrors } from '../../metadata/utils/ajv';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
import { parsedYamlFromWorkflowContents } from './parse';
|
||||
|
||||
const jobTypesWithBuildProfile = new Set(['build', 'repack']);
|
||||
|
||||
@@ -162,11 +163,6 @@ function validateWorkflowStructure(parsedYaml: any, workflowJsonSchema: any): vo
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
import { App, User, UserRole } from '@expo/apple-utils';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { AppStoreApiKeyPurpose } from '../credentials/ios/actions/AscApiKeyUtils';
|
||||
import { getAppFromContextAsync } from '../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { SetUpAscApiKey } from '../credentials/ios/actions/SetUpAscApiKey';
|
||||
import { SetUpBuildCredentials } from '../credentials/ios/actions/SetUpBuildCredentials';
|
||||
import { SetUpPushKey } from '../credentials/ios/actions/SetUpPushKey';
|
||||
import { ensureAppExistsAsync } from '../credentials/ios/appstore/ensureAppExists';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import {
|
||||
WorkflowJobStatus,
|
||||
WorkflowProjectSourceType,
|
||||
WorkflowRunStatus,
|
||||
} from '../graphql/generated';
|
||||
import { AppMutation } from '../graphql/mutations/AppMutation';
|
||||
import { WorkflowRunMutation } from '../graphql/mutations/WorkflowRunMutation';
|
||||
import { AppQuery } from '../graphql/queries/AppQuery';
|
||||
import { WorkflowRunQuery } from '../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { ora } from '../ora';
|
||||
import { getPrivateExpoConfigAsync } from '../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { uploadAccountScopedFileAsync } from '../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { resolveVcsClient } from '../vcs';
|
||||
import { Client as VcsClient } from '../vcs/vcs';
|
||||
|
||||
// Expo Go release info - update when releasing a new version
|
||||
const EXPO_GO_SDK_VERSION = '55';
|
||||
const EXPO_GO_APP_VERSION = '55.0.11';
|
||||
const EXPO_GO_BUILD_NUMBER = '1017799';
|
||||
|
||||
const TESTFLIGHT_GROUP_NAME = 'Team (Expo)';
|
||||
|
||||
async function setupTestFlightAsync(ascApp: App): Promise<void> {
|
||||
// Create or get TestFlight group
|
||||
let group;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
const groups = await ascApp.getBetaGroupsAsync({
|
||||
query: { includes: ['betaTesters'] },
|
||||
});
|
||||
|
||||
group = groups.find(
|
||||
g => g.attributes.isInternalGroup && g.attributes.name === TESTFLIGHT_GROUP_NAME
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
group = await ascApp.createBetaGroupAsync({
|
||||
name: TESTFLIGHT_GROUP_NAME,
|
||||
isInternalGroup: true,
|
||||
hasAccessToAllBuilds: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
} catch (error: any) {
|
||||
// Apple returns this error when the app isn't ready yet
|
||||
if (error?.data?.errors?.some((e: any) => e.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID')) {
|
||||
if (attempt < 9) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
throw new Error('Failed to create TestFlight group');
|
||||
}
|
||||
|
||||
const users = await User.getAsync(ascApp.context);
|
||||
const admins = users.filter(u => u.attributes.roles?.includes(UserRole.ADMIN));
|
||||
|
||||
const existingEmails = new Set(
|
||||
group.attributes.betaTesters?.map((t: any) => t.attributes.email?.toLowerCase()) ?? []
|
||||
);
|
||||
|
||||
const newTesters = admins
|
||||
.filter(u => u.attributes.email && !existingEmails.has(u.attributes.email.toLowerCase()))
|
||||
.map(u => ({
|
||||
email: u.attributes.email!,
|
||||
firstName: u.attributes.firstName ?? '',
|
||||
lastName: u.attributes.lastName ?? '',
|
||||
}));
|
||||
|
||||
if (newTesters.length > 0) {
|
||||
await group.createBulkBetaTesterAssignmentsAsync(newTesters);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
async function withSuppressedOutputAsync<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
let capturedOutput = '';
|
||||
|
||||
const capture = (chunk: any): boolean => {
|
||||
if (typeof chunk === 'string') {
|
||||
capturedOutput += chunk;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Only suppress stdout, not stderr — ora writes spinner frames to stderr and
|
||||
// patching it would freeze the spinner animation during suppressed async work.
|
||||
process.stdout.write = capture as any;
|
||||
console.log = () => {};
|
||||
console.error = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
if (capturedOutput) {
|
||||
originalConsoleLog(capturedOutput);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
|
||||
export default class Go extends EasCommand {
|
||||
static override description = 'Create a custom Expo Go and submit to TestFlight';
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
'bundle-id': Flags.string({
|
||||
description: 'iOS bundle identifier (auto-generated if not provided)',
|
||||
required: false,
|
||||
}),
|
||||
name: Flags.string({
|
||||
description: 'App name',
|
||||
default: 'My Expo Go',
|
||||
}),
|
||||
credentials: Flags.boolean({
|
||||
description: 'Interactively select credentials (default: auto-select)',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.log(
|
||||
chalk.bold(
|
||||
`Creating your personal Expo Go and deploying to TestFlight. ${learnMore('https://expo.fyi/deploy-expo-go-testflight')}`
|
||||
)
|
||||
);
|
||||
|
||||
const { flags } = await this.parse(Go);
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
analytics,
|
||||
} = await this.getContextAsync(Go, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
Log.withTick(`Logged in as ${chalk.cyan(getActorDisplayName(actor))}`);
|
||||
|
||||
const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor);
|
||||
const appName = flags.name ?? 'My Expo Go';
|
||||
const slug = bundleId.split('.').pop() || 'my-expo-go';
|
||||
|
||||
const projectDir = path.join(os.tmpdir(), `eas-go-${slug}`);
|
||||
await fs.emptyDir(projectDir);
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(projectDir);
|
||||
|
||||
const setupSpinner = ora('Creating project...').start();
|
||||
|
||||
// Step 1: Create project files and initialize git (silently)
|
||||
try {
|
||||
await withSuppressedOutputAsync(async () => {
|
||||
await this.createProjectFilesAsync(projectDir, bundleId, appName);
|
||||
await this.initGitRepoAsync(projectDir);
|
||||
});
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
// Step 2: Create/link EAS project (silently)
|
||||
const projectId = await withSuppressedOutputAsync(() =>
|
||||
this.ensureEasProjectAsync(graphqlClient, actor, projectDir, bundleId)
|
||||
);
|
||||
|
||||
// Step 3: Set up iOS credentials and create App Store Connect app
|
||||
const ascApp = await this.setupCredentialsAsync(
|
||||
projectDir,
|
||||
projectId,
|
||||
bundleId,
|
||||
appName,
|
||||
graphqlClient,
|
||||
actor,
|
||||
analytics,
|
||||
vcsClient,
|
||||
flags.credentials,
|
||||
() => {
|
||||
setupSpinner.stop();
|
||||
Log.markFreshLine();
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: Start workflow and monitor progress
|
||||
const { workflowUrl, workflowRunId } = await this.runWorkflowAsync(
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
actor,
|
||||
vcsClient
|
||||
);
|
||||
Log.withTick(`Workflow started: ${chalk.cyan(workflowUrl)}`);
|
||||
|
||||
const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId);
|
||||
if (status === WorkflowRunStatus.Failure) {
|
||||
throw new Error('Workflow failed');
|
||||
} else if (status === WorkflowRunStatus.Canceled) {
|
||||
throw new Error('Workflow was canceled');
|
||||
}
|
||||
|
||||
// Step 5: Set up TestFlight group (silently)
|
||||
try {
|
||||
await setupTestFlightAsync(ascApp);
|
||||
} catch {
|
||||
// Non-fatal: TestFlight group setup failure shouldn't block the user
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.succeed(
|
||||
`Done! Your custom Expo Go has been submitted to TestFlight. ${learnMore(
|
||||
`https://appstoreconnect.apple.com/apps/${ascApp.id}/testflight`,
|
||||
{ learnMoreMessage: 'Open it on App Store Connect' }
|
||||
)}`
|
||||
);
|
||||
Log.log(
|
||||
`App Store processing may take several minutes to complete. ${learnMore(
|
||||
'https://expo.fyi/personal-expo-go',
|
||||
{ learnMoreMessage: 'Learn more about Expo Go on TestFlight' }
|
||||
)}`
|
||||
);
|
||||
|
||||
await fs.remove(projectDir);
|
||||
} catch (error) {
|
||||
Log.gray(`Project files preserved for debugging: ${projectDir}`);
|
||||
throw error;
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
|
||||
private generateBundleId(actor: Actor): string {
|
||||
const username = actor.accounts[0].name;
|
||||
// Sanitize username for bundle ID: only alphanumeric and hyphens allowed
|
||||
const sanitizedUsername = username
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
.replace(/^-+|-+$/g, ''); // trim leading/trailing hyphens
|
||||
// Deterministic bundle ID per user + SDK version (reuses same ASC app)
|
||||
return `com.${sanitizedUsername || 'app'}.expogo${EXPO_GO_SDK_VERSION}`;
|
||||
}
|
||||
|
||||
private async createProjectFilesAsync(
|
||||
projectDir: string,
|
||||
bundleId: string,
|
||||
appName: string
|
||||
): Promise<void> {
|
||||
const slug = bundleId.split('.').pop() || 'custom-expo-go';
|
||||
const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
|
||||
|
||||
const appJson = {
|
||||
expo: {
|
||||
name: appName,
|
||||
slug,
|
||||
version: EXPO_GO_APP_VERSION,
|
||||
ios: {
|
||||
bundleIdentifier: bundleId,
|
||||
buildNumber: EXPO_GO_BUILD_NUMBER,
|
||||
config: {
|
||||
usesNonExemptEncryption: false,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
eas: {
|
||||
build: {
|
||||
experimental: {
|
||||
ios: {
|
||||
appExtensions: [
|
||||
{
|
||||
targetName: 'ExpoNotificationServiceExtension',
|
||||
bundleIdentifier: extensionBundleId,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const easJson = {
|
||||
cli: {
|
||||
version: '>= 5.0.0',
|
||||
},
|
||||
build: {
|
||||
production: {
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {
|
||||
ios: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const packageJson = {
|
||||
name: slug,
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
expo: '~54.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
await fs.writeJson(path.join(projectDir, 'app.json'), appJson, { spaces: 2 });
|
||||
await fs.writeJson(path.join(projectDir, 'eas.json'), easJson, { spaces: 2 });
|
||||
await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
|
||||
|
||||
await spawnAsync('npm', ['install'], { cwd: projectDir });
|
||||
}
|
||||
|
||||
private async initGitRepoAsync(projectDir: string): Promise<void> {
|
||||
await spawnAsync('git', ['init'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['add', '.'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['commit', '-m', 'Initial commit'], { cwd: projectDir });
|
||||
}
|
||||
|
||||
private async ensureEasProjectAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
projectDir: string,
|
||||
bundleId: string
|
||||
): Promise<string> {
|
||||
const slug = bundleId.split('.').pop() || 'custom-expo-go';
|
||||
const account = actor.accounts[0];
|
||||
|
||||
const existingProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
account.name,
|
||||
slug
|
||||
);
|
||||
|
||||
if (existingProjectId) {
|
||||
await saveProjectIdToAppConfigAsync(projectDir, existingProjectId);
|
||||
return existingProjectId;
|
||||
}
|
||||
|
||||
const projectId = await AppMutation.createAppAsync(graphqlClient, {
|
||||
accountId: account.id,
|
||||
projectName: slug,
|
||||
});
|
||||
await saveProjectIdToAppConfigAsync(projectDir, projectId);
|
||||
return projectId;
|
||||
}
|
||||
|
||||
private async setupCredentialsAsync(
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
bundleId: string,
|
||||
appName: string,
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
analytics: Analytics,
|
||||
vcsClient: VcsClient,
|
||||
customizeCreds: boolean,
|
||||
onBeforeAppleAuth?: () => void
|
||||
): Promise<App> {
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir);
|
||||
const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive: false,
|
||||
autoAcceptCredentialReuse: !customizeCreds,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
});
|
||||
|
||||
onBeforeAppleAuth?.();
|
||||
const userAuthCtx = await credentialsCtx.appStore.ensureUserAuthenticatedAsync();
|
||||
|
||||
const app = await getAppFromContextAsync(credentialsCtx);
|
||||
|
||||
const targets: Target[] = [
|
||||
{
|
||||
targetName: exp.slug,
|
||||
bundleIdentifier: bundleId,
|
||||
entitlements: {},
|
||||
},
|
||||
{
|
||||
targetName: 'ExpoNotificationServiceExtension',
|
||||
bundleIdentifier: extensionBundleId,
|
||||
parentBundleIdentifier: bundleId,
|
||||
entitlements: {},
|
||||
},
|
||||
];
|
||||
|
||||
const ascApp = await withSuppressedOutputAsync(async () => {
|
||||
await new SetUpBuildCredentials({
|
||||
app,
|
||||
targets,
|
||||
distribution: 'store',
|
||||
}).runAsync(credentialsCtx);
|
||||
|
||||
const appLookupParams = {
|
||||
...app,
|
||||
bundleIdentifier: bundleId,
|
||||
};
|
||||
await new SetUpAscApiKey(appLookupParams, AppStoreApiKeyPurpose.SUBMISSION_SERVICE).runAsync(
|
||||
credentialsCtx
|
||||
);
|
||||
|
||||
const ascAppResult = await ensureAppExistsAsync(userAuthCtx, {
|
||||
name: appName,
|
||||
bundleIdentifier: bundleId,
|
||||
});
|
||||
|
||||
const easJsonPath = path.join(projectDir, 'eas.json');
|
||||
const easJson = await fs.readJson(easJsonPath);
|
||||
easJson.submit = easJson.submit || {};
|
||||
easJson.submit.production = easJson.submit.production || {};
|
||||
easJson.submit.production.ios = easJson.submit.production.ios || {};
|
||||
easJson.submit.production.ios.ascAppId = ascAppResult.id;
|
||||
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
|
||||
|
||||
await spawnAsync('git', ['add', 'eas.json'], { cwd: projectDir });
|
||||
await spawnAsync('git', ['commit', '-m', 'Add ascAppId to eas.json'], { cwd: projectDir });
|
||||
|
||||
return ascAppResult;
|
||||
});
|
||||
|
||||
// Set up push notifications (outside suppressed block so prompts are visible)
|
||||
const appLookupParamsForPushKey = { ...app, bundleIdentifier: bundleId };
|
||||
const setupPushKeyAction = new SetUpPushKey(appLookupParamsForPushKey);
|
||||
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(credentialsCtx);
|
||||
if (!isPushKeySetup) {
|
||||
if (customizeCreds) {
|
||||
const wantsPushNotifications = await confirmAsync({
|
||||
message: 'Would you like to set up Push Notifications for your app?',
|
||||
initial: true,
|
||||
});
|
||||
if (wantsPushNotifications) {
|
||||
await setupPushKeyAction.runAsync(credentialsCtx);
|
||||
}
|
||||
} else {
|
||||
await setupPushKeyAction.runAsync(credentialsCtx);
|
||||
}
|
||||
}
|
||||
|
||||
return ascApp;
|
||||
}
|
||||
|
||||
private async runWorkflowAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectDir: string,
|
||||
projectId: string,
|
||||
actor: Actor,
|
||||
vcsClient: VcsClient
|
||||
): Promise<{ workflowUrl: string; workflowRunId: string }> {
|
||||
const account = actor.accounts[0];
|
||||
|
||||
const { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey } =
|
||||
await withSuppressedOutputAsync(async () => {
|
||||
const { projectArchiveBucketKey } = await uploadAccountScopedProjectSourceAsync({
|
||||
graphqlClient,
|
||||
vcsClient,
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
const { fileBucketKey: easJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: path.join(projectDir, 'eas.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
});
|
||||
|
||||
const { fileBucketKey: packageJsonBucketKey } = await uploadAccountScopedFileAsync({
|
||||
graphqlClient,
|
||||
accountId: account.id,
|
||||
filePath: path.join(projectDir, 'package.json'),
|
||||
maxSizeBytes: 1024 * 1024,
|
||||
});
|
||||
|
||||
return { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey };
|
||||
});
|
||||
|
||||
const { id: workflowRunId } = await WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: projectId,
|
||||
projectSource: {
|
||||
type: WorkflowProjectSourceType.Gcs,
|
||||
projectArchiveBucketKey,
|
||||
easJsonBucketKey,
|
||||
packageJsonBucketKey,
|
||||
projectRootDirectory: '.',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
const workflowUrl = `https://expo.dev/accounts/${account.name}/projects/${app.slug}/workflows/${workflowRunId}`;
|
||||
|
||||
return { workflowUrl, workflowRunId };
|
||||
}
|
||||
|
||||
private async monitorWorkflowJobsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRunId: string
|
||||
): Promise<WorkflowRunStatus> {
|
||||
const EXPECTED_BUILD_DURATION_SECONDS = 5 * 60;
|
||||
const EXPECTED_SUBMIT_DURATION_SECONDS = 2 * 60;
|
||||
const buildStartTime = Date.now();
|
||||
let submitStartTime: number | null = null;
|
||||
|
||||
const buildSpinner = ora(
|
||||
this.formatSpinnerText('Building Expo Go', EXPECTED_BUILD_DURATION_SECONDS, buildStartTime)
|
||||
).start();
|
||||
let submitSpinner: ReturnType<typeof ora> | null = null;
|
||||
let buildCompleted = false;
|
||||
let failedFetchesCount = 0;
|
||||
|
||||
while (true) {
|
||||
if (!buildCompleted) {
|
||||
buildSpinner.text = this.formatSpinnerText(
|
||||
'Building Expo Go',
|
||||
EXPECTED_BUILD_DURATION_SECONDS,
|
||||
buildStartTime
|
||||
);
|
||||
}
|
||||
if (submitSpinner && submitStartTime) {
|
||||
submitSpinner.text = this.formatSpinnerText(
|
||||
'Submitting to TestFlight',
|
||||
EXPECTED_SUBMIT_DURATION_SECONDS,
|
||||
submitStartTime
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
failedFetchesCount = 0;
|
||||
|
||||
const repackJob = workflowRun.jobs.find(j => j.name === 'Repack Expo Go');
|
||||
const submitJob = workflowRun.jobs.find(j => j.name === 'Submit to TestFlight');
|
||||
|
||||
if (!buildCompleted) {
|
||||
if (repackJob?.status === WorkflowJobStatus.Success) {
|
||||
buildSpinner.succeed('Built Expo Go');
|
||||
buildCompleted = true;
|
||||
} else if (
|
||||
repackJob?.status === WorkflowJobStatus.Failure ||
|
||||
repackJob?.status === WorkflowJobStatus.Canceled
|
||||
) {
|
||||
buildSpinner.fail('Build failed');
|
||||
return WorkflowRunStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
if (buildCompleted && submitSpinner === null && submitJob) {
|
||||
submitStartTime = Date.now();
|
||||
submitSpinner = ora(
|
||||
this.formatSpinnerText(
|
||||
'Submitting to TestFlight',
|
||||
EXPECTED_SUBMIT_DURATION_SECONDS,
|
||||
submitStartTime
|
||||
)
|
||||
).start();
|
||||
}
|
||||
|
||||
if (workflowRun.status === WorkflowRunStatus.Success) {
|
||||
submitSpinner?.stop();
|
||||
return WorkflowRunStatus.Success;
|
||||
} else if (workflowRun.status === WorkflowRunStatus.Failure) {
|
||||
buildSpinner.stop();
|
||||
submitSpinner?.fail('Submission failed');
|
||||
return WorkflowRunStatus.Failure;
|
||||
} else if (workflowRun.status === WorkflowRunStatus.Canceled) {
|
||||
buildSpinner.stop();
|
||||
submitSpinner?.stop();
|
||||
return WorkflowRunStatus.Canceled;
|
||||
}
|
||||
} catch {
|
||||
failedFetchesCount++;
|
||||
if (failedFetchesCount > 6) {
|
||||
buildSpinner.fail();
|
||||
submitSpinner?.fail();
|
||||
throw new Error('Failed to fetch the workflow run status 6 times in a row');
|
||||
}
|
||||
}
|
||||
|
||||
await sleepAsync(10 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private formatSpinnerText(
|
||||
label: string,
|
||||
expectedDurationSeconds: number,
|
||||
startTime: number
|
||||
): string {
|
||||
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
||||
const remainingSeconds = Math.max(0, expectedDurationSeconds - elapsedSeconds);
|
||||
|
||||
if (remainingSeconds === 0) {
|
||||
return `${label} (almost done...)`;
|
||||
}
|
||||
|
||||
const minutes = Math.ceil(remainingSeconds / 60);
|
||||
const unit = minutes === 1 ? 'minute' : 'minutes';
|
||||
return `${label} (~${minutes} ${unit} remaining)`;
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,67 @@ describe(UpdatePublish.name, () => {
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors when SDK >= 55 and --environment is not provided', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
|
||||
|
||||
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'--environment flag is required for projects using Expo SDK 55 or greater'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not error when SDK >= 55 and --environment is provided', async () => {
|
||||
const flags = [
|
||||
'--non-interactive',
|
||||
'--branch=branch123',
|
||||
'--message=abc',
|
||||
'--environment=production',
|
||||
];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '55.0.0' } });
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not error when SDK < 55 and --environment is not provided', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject({ expoConfig: { sdkVersion: '54.0.0' } });
|
||||
const { platforms, runtimeVersion } = mockTestExport();
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branch: {
|
||||
id: 'branch123',
|
||||
name: 'wat',
|
||||
},
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a new update with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags, EasUpdateEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
EasUpdateEnvironmentRequiredFlag,
|
||||
} from '../../commandUtils/flags';
|
||||
import { assertEnvironmentFlagForSdk55OrGreater } from '../../update/utils';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
@@ -190,7 +194,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory. Only relevant if you are using code signing: https://docs.expo.dev/eas-update/code-signing/`,
|
||||
required: false,
|
||||
}),
|
||||
...EasUpdateEnvironmentFlag,
|
||||
...EasUpdateEnvironmentRequiredFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
@@ -249,6 +253,11 @@ export default class UpdatePublish extends EasCommand {
|
||||
projectDir,
|
||||
} = await getDynamicPublicProjectConfigAsync();
|
||||
|
||||
assertEnvironmentFlagForSdk55OrGreater({
|
||||
sdkVersion: expPossiblyWithoutEasUpdateConfigured.sdkVersion,
|
||||
environment,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
|
||||
@@ -25,6 +25,7 @@ export class CredentialsContext {
|
||||
public readonly appStore = new AppStoreApi();
|
||||
public readonly ios = IosGraphqlClient;
|
||||
public readonly nonInteractive: boolean;
|
||||
public readonly autoAcceptCredentialReuse: boolean;
|
||||
public readonly freezeCredentials: boolean = false;
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
@@ -50,6 +51,7 @@ export class CredentialsContext {
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
freezeCredentials?: boolean;
|
||||
autoAcceptCredentialReuse?: boolean;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
@@ -60,6 +62,7 @@ export class CredentialsContext {
|
||||
this.analytics = options.analytics;
|
||||
this.vcsClient = options.vcsClient;
|
||||
this.nonInteractive = options.nonInteractive ?? false;
|
||||
this.autoAcceptCredentialReuse = options.autoAcceptCredentialReuse ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
this.freezeCredentials = options.freezeCredentials ?? false;
|
||||
this.usesBroadcastPushNotifications =
|
||||
|
||||
@@ -66,6 +66,11 @@ export async function provideOrGenerateAscApiKeyAsync(
|
||||
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
|
||||
}
|
||||
|
||||
// When auto-accepting credentials, always auto-generate without asking for user input
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
}
|
||||
|
||||
const userProvided = await promptForAscApiKeyAsync(ctx);
|
||||
if (!userProvided) {
|
||||
return await generateAscApiKeyAsync(ctx, purpose);
|
||||
|
||||
@@ -64,7 +64,7 @@ export class CreateProvisioningProfile {
|
||||
private async maybeGetUserProvidedAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<ProvisioningProfile | null> {
|
||||
if (ctx.nonInteractive) {
|
||||
if (ctx.nonInteractive || ctx.autoAcceptCredentialReuse) {
|
||||
return null;
|
||||
}
|
||||
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
|
||||
|
||||
@@ -162,7 +162,7 @@ Remember that Apple Distribution Certificates are not application specific!
|
||||
export async function provideOrGenerateDistributionCertificateAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<DistributionCertificate> {
|
||||
if (!ctx.nonInteractive) {
|
||||
if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
|
||||
const userProvided = await promptForDistCertAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { pushKeySchema } from '../credentials';
|
||||
import { isPushKeyValidAndTrackedAsync } from '../validators/validatePushKey';
|
||||
|
||||
export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Promise<PushKey> {
|
||||
if (!ctx.nonInteractive) {
|
||||
if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
|
||||
const userProvided = await promptForPushKeyAsync(ctx);
|
||||
if (userProvided) {
|
||||
if (!ctx.appStore.authCtx) {
|
||||
|
||||
@@ -65,6 +65,12 @@ export class SetUpAscApiKey {
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
|
||||
}
|
||||
|
||||
// When auto-accepting credentials and no valid key was found, generate a new one without prompting
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
const ascApiKey = await new CreateAscApiKey(this.app.account).runAsync(ctx, this.purpose);
|
||||
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
|
||||
}
|
||||
|
||||
const availableChoices =
|
||||
keysForAccount.length === 0
|
||||
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
|
||||
@@ -90,9 +96,11 @@ export class SetUpAscApiKey {
|
||||
return null;
|
||||
}
|
||||
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this App Store Connect API Key?\n${formatAscApiKey(autoselectedKey)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using App Store Connect API Key with ID ${autoselectedKey.keyIdentifier}`);
|
||||
|
||||
@@ -117,11 +117,13 @@ export class SetUpDistributionCertificate {
|
||||
const validDistCerts = await this.getValidDistCertsAsync(ctx);
|
||||
const autoselectedDistCert = validDistCerts[0];
|
||||
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this distribution certificate?\n${formatDistributionCertificate(
|
||||
autoselectedDistCert
|
||||
)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this distribution certificate?\n${formatDistributionCertificate(
|
||||
autoselectedDistCert
|
||||
)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(
|
||||
|
||||
@@ -142,7 +142,7 @@ export class SetUpProvisioningProfile {
|
||||
}
|
||||
|
||||
const isNonInteractiveOrUserDidConfirmAsync = async (): Promise<boolean> => {
|
||||
if (ctx.nonInteractive) {
|
||||
if (ctx.nonInteractive || ctx.autoAcceptCredentialReuse) {
|
||||
return true;
|
||||
}
|
||||
return await confirmAsync({
|
||||
|
||||
@@ -63,12 +63,14 @@ export class SetUpPushKey {
|
||||
if (validPushKeys.length > 0) {
|
||||
const autoselectedPushKey = validPushKeys[0];
|
||||
|
||||
const useAutoselected = await confirmAsync({
|
||||
message: `Reuse this Push Key?\n${formatPushKey(
|
||||
autoselectedPushKey,
|
||||
validPushKeys.map(pushKey => pushKey.keyIdentifier)
|
||||
)}`,
|
||||
});
|
||||
const useAutoselected =
|
||||
ctx.autoAcceptCredentialReuse ||
|
||||
(await confirmAsync({
|
||||
message: `Reuse this Push Key?\n${formatPushKey(
|
||||
autoselectedPushKey,
|
||||
validPushKeys.map(pushKey => pushKey.keyIdentifier)
|
||||
)}`,
|
||||
}));
|
||||
|
||||
if (useAutoselected) {
|
||||
Log.log(`Using push key with ID ${autoselectedPushKey.keyIdentifier}`);
|
||||
@@ -77,6 +79,11 @@ export class SetUpPushKey {
|
||||
}
|
||||
}
|
||||
|
||||
// When auto-accepting and no valid push key was found, generate a new one without prompting
|
||||
if (ctx.autoAcceptCredentialReuse) {
|
||||
return await new CreatePushKey(this.app.account).runAsync(ctx);
|
||||
}
|
||||
|
||||
const { action } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'action',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateWorkflowRunFromGitRefMutationVariables,
|
||||
CreateWorkflowRunMutation,
|
||||
CreateWorkflowRunMutationVariables,
|
||||
WorkflowProjectSourceInput,
|
||||
WorkflowRevisionInput,
|
||||
WorkflowRunInput,
|
||||
} from '../generated';
|
||||
@@ -103,6 +104,40 @@ export namespace WorkflowRunMutation {
|
||||
return { id: data.workflowRun.createWorkflowRunFromGitRef.id };
|
||||
}
|
||||
|
||||
export async function createExpoGoRepackWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
projectSource,
|
||||
}: {
|
||||
appId: string;
|
||||
projectSource: WorkflowProjectSourceInput;
|
||||
}
|
||||
): Promise<{ id: string }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ workflowRun: { createExpoGoRepackWorkflowRun: { id: string } } },
|
||||
{ appId: string; projectSource: WorkflowProjectSourceInput }
|
||||
>(
|
||||
/* eslint-disable graphql/template-strings */
|
||||
gql`
|
||||
mutation CreateExpoGoRepackWorkflowRun($appId: ID!, $projectSource: WorkflowProjectSourceInput!) {
|
||||
workflowRun {
|
||||
createExpoGoRepackWorkflowRun(appId: $appId, projectSource: $projectSource) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
/* eslint-enable graphql/template-strings */
|
||||
{ appId, projectSource }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return { id: data.workflowRun.createExpoGoRepackWorkflowRun.id };
|
||||
}
|
||||
|
||||
export async function cancelWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
|
||||
@@ -26,6 +26,15 @@ export default class Log {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the cursor is already at the start of a fresh line (e.g. after
|
||||
* a spinner clears itself), so that the next `addNewLineIfNone()` call knows
|
||||
* it doesn't need to emit an extra blank line.
|
||||
*/
|
||||
public static markFreshLine(): void {
|
||||
Log.isLastLineNewLine = true;
|
||||
}
|
||||
|
||||
public static error(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Errors } from '@oclif/core';
|
||||
import { format } from '@expo/timeago.js';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -357,3 +359,20 @@ export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, A
|
||||
android: AppPlatform.Android,
|
||||
ios: AppPlatform.Ios,
|
||||
};
|
||||
|
||||
export function assertEnvironmentFlagForSdk55OrGreater({
|
||||
sdkVersion,
|
||||
environment,
|
||||
}: {
|
||||
sdkVersion: string | undefined;
|
||||
environment: string | undefined;
|
||||
}): void {
|
||||
if (process.env.EAS_BUILD) {
|
||||
return;
|
||||
}
|
||||
if (sdkVersion && semver.gte(sdkVersion, '55.0.0') && !environment) {
|
||||
Errors.error('--environment flag is required for projects using Expo SDK 55 or greater', {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"version": "18.0.1",
|
||||
"version": "18.0.2",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.23.5",
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
@@ -47,6 +47,7 @@
|
||||
"@tsconfig/node20": "20.1.8",
|
||||
"@types/babel__code-frame": "7.0.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"jest": "29.7.0",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "5.5.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "18.0.1",
|
||||
"version": "18.1.0",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -10,9 +10,7 @@
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/local-build-plugin"
|
||||
},
|
||||
"bin": {
|
||||
"eas-cli-local-build-plugin": "./bin/run"
|
||||
},
|
||||
"bin": "./bin/run",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
@@ -28,8 +26,8 @@
|
||||
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/build-tools": "18.1.0",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "18.0.1",
|
||||
"version": "18.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -27,7 +27,7 @@
|
||||
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"arg": "^5.0.2",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Build Worker
|
||||
|
||||
defaults:
|
||||
tools:
|
||||
corepack: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "18.0.1",
|
||||
"version": "18.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,16 +19,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.0.1",
|
||||
"@expo/build-tools": "18.1.0",
|
||||
"@expo/config-plugins": "8.0.8",
|
||||
"@expo/config-types": "52.0.1",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.1",
|
||||
"@expo/eas-build-job": "18.0.2",
|
||||
"@expo/logger": "18.0.1",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "18.0.1",
|
||||
"@expo/steps": "18.0.2",
|
||||
"@expo/template-file": "18.0.1",
|
||||
"@expo/turtle-spawn": "18.0.1",
|
||||
"@hapi/boom": "10.0.1",
|
||||
@@ -49,6 +49,7 @@
|
||||
"semver": "7.6.3",
|
||||
"tar": "7.5.7",
|
||||
"uuid": "3.4.0",
|
||||
"uuidv7": "1.1.0",
|
||||
"ws": "8.18.0",
|
||||
"xml2js": "0.6.2",
|
||||
"zod": "4.1.8"
|
||||
|
||||
@@ -45,21 +45,42 @@ copy_package() {
|
||||
fi
|
||||
}
|
||||
|
||||
copy_manifest() {
|
||||
src=$1
|
||||
dst=$2
|
||||
mkdir -p $dst
|
||||
cp "$src/package.json" "$dst/package.json"
|
||||
if [[ -f "$src/tsconfig.json" ]]; then
|
||||
cp "$src/tsconfig.json" "$dst/tsconfig.json"
|
||||
fi
|
||||
if [[ -f "$src/tsconfig.build.json" ]]; then
|
||||
cp "$src/tsconfig.build.json" "$dst/tsconfig.build.json"
|
||||
fi
|
||||
}
|
||||
|
||||
cp "$ROOT_DIR/yarn.lock" $target_root_dir
|
||||
cp "$ROOT_DIR/.yarnrc.yml" $target_root_dir
|
||||
cp "$ROOT_DIR/package.json" $target_root_dir
|
||||
cp "$ROOT_DIR/lerna.json" $target_root_dir
|
||||
|
||||
for package_dir in "$ROOT_DIR"/packages/*; do
|
||||
if [[ -f "$package_dir/package.json" ]]; then
|
||||
package_name=$(basename "$package_dir")
|
||||
copy_manifest "$package_dir" "$target_root_dir/packages/$package_name"
|
||||
fi
|
||||
done
|
||||
|
||||
copy_package $WORKER_DIR $target_worker_dir
|
||||
while IFS= read -r package_dir; do
|
||||
package_name=$(basename "$package_dir")
|
||||
copy_package "$package_dir" "$target_root_dir/packages/$package_name"
|
||||
done <<< "$(yarn --silent lerna list --scope "@expo/worker" --include-dependencies --parseable --loglevel silent)"
|
||||
done <<< "$(cd "$ROOT_DIR" && yarn run -T lerna list --scope "@expo/worker" --include-dependencies --parseable --loglevel silent)"
|
||||
|
||||
|
||||
pushd $target_root_dir >/dev/null 2>&1
|
||||
yarn install --silent --frozen-lockfile
|
||||
yarn build
|
||||
yarn install --silent --production=true --frozen-lockfile
|
||||
yarn install --immutable
|
||||
yarn run -T lerna run build --scope @expo/worker --include-dependencies
|
||||
yarn workspaces focus @expo/worker --production
|
||||
rm -rf tsconfig.json tsconfig.build.json
|
||||
popd >/dev/null 2>&1
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { TransformCallback, Writable } from 'node:stream';
|
||||
import { createBuildLoggerWithSecretsFilter } from '../logger';
|
||||
import z from 'zod';
|
||||
import { EnvironmentSecretType } from '@expo/eas-build-job';
|
||||
|
||||
async function waitForStreamFlush(): Promise<void> {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
|
||||
describe('logger', () => {
|
||||
it('obfuscates secrets in logs', async () => {
|
||||
const { logger, outputStream } = await createBuildLoggerWithSecretsFilter([
|
||||
{ name: 'TEST_SECRET', value: 'secret', type: EnvironmentSecretType.STRING },
|
||||
{
|
||||
name: 'ANOTHER_SECRET_BASE64',
|
||||
value: 'YW5vdGhlclNlY3JldA==',
|
||||
type: EnvironmentSecretType.STRING,
|
||||
},
|
||||
]);
|
||||
|
||||
const logs: any[] = [];
|
||||
|
||||
const writable = new Writable({
|
||||
objectMode: true,
|
||||
write(chunk: any, _encoding: BufferEncoding, callback: TransformCallback) {
|
||||
logs.push(chunk);
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
|
||||
outputStream.pipe(writable);
|
||||
|
||||
logger.info('this is a secret');
|
||||
logger.info(`another secret in base64 is ${Buffer.from('anotherSecret').toString('base64')}`);
|
||||
|
||||
await waitForStreamFlush();
|
||||
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs[0].msg).toBe('this is a ******');
|
||||
expect(logs[1].msg).toBe('another ****** in base64 is ********************');
|
||||
});
|
||||
|
||||
it('adds logId to each log', async () => {
|
||||
const { logger, outputStream } = await createBuildLoggerWithSecretsFilter([]);
|
||||
|
||||
const logs: any[] = [];
|
||||
|
||||
const writable = new Writable({
|
||||
objectMode: true,
|
||||
write(chunk: any, _encoding: BufferEncoding, callback: TransformCallback) {
|
||||
logs.push(chunk);
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
|
||||
outputStream.pipe(writable);
|
||||
|
||||
logger.info('Test log');
|
||||
logger.info('Test log');
|
||||
|
||||
await waitForStreamFlush();
|
||||
|
||||
expect(logs.length).toBe(2);
|
||||
|
||||
for (const log of logs) {
|
||||
expect(log.logId).toBeDefined();
|
||||
expect(z.uuidv7().parse(log.logId)).toBe(log.logId);
|
||||
}
|
||||
|
||||
expect(logs[0].logId).not.toBe(logs[1].logId);
|
||||
});
|
||||
|
||||
it('drains transformed logs even without explicit output consumer', async () => {
|
||||
const { logger, outputStream } = await createBuildLoggerWithSecretsFilter([]);
|
||||
|
||||
logger.info('Test log');
|
||||
logger.info('Test log');
|
||||
logger.info('Test log');
|
||||
|
||||
await waitForStreamFlush();
|
||||
|
||||
expect(outputStream.readableLength).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -89,9 +89,21 @@ describe('prepareRuntimeEnvironment', () => {
|
||||
|
||||
await prepareRuntimeEnvironment(ctx, { bun: '2.0.0' }, false);
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith('yarn', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith('pnpm', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith('bun', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pnpm',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'bun',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'curl',
|
||||
@@ -123,9 +135,21 @@ describe('prepareRuntimeEnvironment', () => {
|
||||
|
||||
await prepareRuntimeEnvironment(ctx, { bun: '2.0.0' }, false);
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith('yarn', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith('pnpm', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith('bun', ['--version'], { stdio: 'pipe' });
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'pnpm',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'bun',
|
||||
['--version'],
|
||||
expect.objectContaining({ stdio: 'pipe', cwd: expect.any(String) })
|
||||
);
|
||||
|
||||
expect(spawn).not.toHaveBeenCalledWith(
|
||||
'curl',
|
||||
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
}),
|
||||
loggers: {
|
||||
base: {
|
||||
name: env('LOGGER_NAME', { defaultValue: 'turtle-worker' }),
|
||||
name: env('LOGGER_NAME', { defaultValue: 'worker' }),
|
||||
uploadIntervalMs: env('LOGGER_INTERVAL_MS', { defaultValue: 10000, transform: Number }),
|
||||
},
|
||||
gcs: {
|
||||
|
||||
+28
-5
@@ -5,6 +5,7 @@ import { ExpoConfig } from '@expo/config-types';
|
||||
import { BuildJob, EnvironmentSecret, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { readFile, readJson } from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
@@ -38,12 +39,13 @@ export async function logProjectDependenciesAsync(
|
||||
const dependencies = (packageJSON?.dependencies as Record<string, string> | undefined) ?? {};
|
||||
const devDependencies =
|
||||
(packageJSON?.devDependencies as Record<string, string> | undefined) ?? {};
|
||||
const projectDirectory = ctx.getReactNativeProjectDirectory();
|
||||
const plugins = filterSecretsAndParsePlugins(
|
||||
ctx.appConfig.plugins,
|
||||
ctx.job.secrets?.environmentSecrets
|
||||
);
|
||||
|
||||
const babelConfig = loadPartialConfig({ cwd: ctx.getReactNativeProjectDirectory() });
|
||||
const babelConfig = loadPartialConfig({ cwd: projectDirectory });
|
||||
const babelPlugins =
|
||||
babelConfig?.options?.plugins
|
||||
?.map(plugin => {
|
||||
@@ -65,7 +67,10 @@ export async function logProjectDependenciesAsync(
|
||||
version: dependency[1],
|
||||
})),
|
||||
packageManager: ctx.packageManager,
|
||||
packageManagerVersion: await getPackageManagerVersion(ctx.packageManager),
|
||||
packageManagerVersion: await getPackageManagerVersion(ctx.packageManager, {
|
||||
cwd: projectDirectory,
|
||||
env: ctx.env,
|
||||
}),
|
||||
jsEngine: resolveJsEngine(ctx, dependencies),
|
||||
newArchEnabled: await resolveNewArchEnabled(ctx, dependencies['react-native']),
|
||||
source: 'Turtle Worker',
|
||||
@@ -202,11 +207,29 @@ function isConfigItem(plugin: PluginItem): plugin is ConfigItem {
|
||||
return !Array.isArray(plugin) && (plugin as any)?.file;
|
||||
}
|
||||
|
||||
async function getPackageManagerVersion(packageManager: string): Promise<string | undefined> {
|
||||
async function getPackageManagerVersion(
|
||||
packageManager: string,
|
||||
{
|
||||
cwd,
|
||||
env,
|
||||
}: {
|
||||
cwd: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version']);
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], { cwd, env });
|
||||
return stdout.toString().trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
// Some package managers can fail version checks in project dirs with mismatched packageManager fields.
|
||||
try {
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], {
|
||||
cwd: os.tmpdir(),
|
||||
env,
|
||||
});
|
||||
return stdout.toString().trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
import { LogBuffer } from '@expo/build-tools';
|
||||
import { EnvironmentSecret } from '@expo/eas-build-job';
|
||||
import { LoggerLevel, createLogger } from '@expo/logger';
|
||||
import { Transform, TransformCallback, Writable } from 'stream';
|
||||
import { GCSLoggerStream, LogBuffer } from '@expo/build-tools';
|
||||
import { BuildPhase, EnvironmentSecret } from '@expo/eas-build-job';
|
||||
import { LoggerLevel, bunyan, createLogger } from '@expo/logger';
|
||||
import { Readable, Transform, TransformCallback, Writable } from 'stream';
|
||||
|
||||
import config from './config';
|
||||
import { maybeStringBase64Decode, simpleSecretsWhitelist } from './secrets';
|
||||
import { BuildLogger as CommonBuildLogger, createGCSBuildLogger } from './utils/logger';
|
||||
|
||||
export interface BuildLogger extends CommonBuildLogger {
|
||||
logBuffer: LogBuffer;
|
||||
}
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const defaultLogger = createLogger({
|
||||
name: config.loggers.base.name,
|
||||
@@ -60,20 +56,33 @@ export class WorkerLogBuffer extends Writable implements LogBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
function createSecretMaskingStream(secrets: EnvironmentSecret[]): Transform {
|
||||
function createTransformStream(secrets: EnvironmentSecret[]): Transform {
|
||||
const secretValues = secrets.map(({ value }) => value);
|
||||
const secretList: string[] = [
|
||||
...secretValues,
|
||||
...secretValues.map(maybeStringBase64Decode).filter((i): i is string => !!i),
|
||||
].filter(i => i.length > 1 && !simpleSecretsWhitelist.includes(i));
|
||||
|
||||
return new Transform({
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: true,
|
||||
transform(chunk: any, _encoding: BufferEncoding, callback: TransformCallback) {
|
||||
transform(_chunk: any, _encoding: BufferEncoding, callback: TransformCallback) {
|
||||
let chunk = _chunk;
|
||||
if (chunk && typeof chunk === 'object') {
|
||||
// Remove hostname from logs since we don't need it.
|
||||
const { hostname: _hostname, ...rest } = chunk;
|
||||
chunk = {
|
||||
...rest,
|
||||
// Add logId to each log.
|
||||
logId: uuidv7(),
|
||||
};
|
||||
}
|
||||
|
||||
if (chunk && typeof chunk === 'object' && chunk.msg) {
|
||||
const msgWithoutSecrets = secretList.reduce((acc: string, pattern: string): string => {
|
||||
return acc.replaceAll(pattern, '*'.repeat(pattern.length));
|
||||
}, chunk.msg as string);
|
||||
|
||||
callback(null, {
|
||||
...chunk,
|
||||
msg: msgWithoutSecrets,
|
||||
@@ -85,32 +94,64 @@ function createSecretMaskingStream(secrets: EnvironmentSecret[]): Transform {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createBuildLoggerWithSecretsFilter(
|
||||
secrets?: EnvironmentSecret[]
|
||||
): Promise<BuildLogger> {
|
||||
const logBuffer = new WorkerLogBuffer(MAX_LINES_IN_BUFFER);
|
||||
const childLogger = defaultLogger.child({ service: 'worker' });
|
||||
|
||||
const { logger, stream } = await createGCSBuildLogger({
|
||||
uploadMethod: config.loggers.gcs.signedUploadUrlForLogs
|
||||
? { signedUrl: config.loggers.gcs.signedUploadUrlForLogs }
|
||||
: undefined,
|
||||
options: {
|
||||
uploadIntervalMs: config.loggers.base.uploadIntervalMs,
|
||||
compress: config.loggers.gcs.compressionMethod,
|
||||
function createDiscardStream(): Writable {
|
||||
return new Writable({
|
||||
objectMode: true,
|
||||
write(_chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
|
||||
callback(null);
|
||||
},
|
||||
transformStream: secrets && createSecretMaskingStream(secrets),
|
||||
logger: childLogger,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createBuildLoggerWithSecretsFilter(secrets?: EnvironmentSecret[]): Promise<{
|
||||
logger: bunyan;
|
||||
cleanUp: () => Promise<void>;
|
||||
logBuffer: WorkerLogBuffer;
|
||||
outputStream: Readable;
|
||||
}> {
|
||||
const buildLogger = defaultLogger.child({ phase: BuildPhase.UNKNOWN });
|
||||
|
||||
const transformStream = createTransformStream(secrets ?? []);
|
||||
const discardStream = createDiscardStream();
|
||||
transformStream.pipe(discardStream);
|
||||
|
||||
buildLogger.addStream({
|
||||
type: 'raw',
|
||||
stream: transformStream,
|
||||
reemitErrorEvents: true,
|
||||
level: buildLogger.level(),
|
||||
});
|
||||
|
||||
logger.addStream({
|
||||
let gcsLoggerStream: GCSLoggerStream | null = null;
|
||||
if (config.loggers.gcs.signedUploadUrlForLogs) {
|
||||
gcsLoggerStream = new GCSLoggerStream({
|
||||
uploadMethod: { signedUrl: config.loggers.gcs.signedUploadUrlForLogs },
|
||||
options: {
|
||||
uploadIntervalMs: config.loggers.base.uploadIntervalMs,
|
||||
compress: config.loggers.gcs.compressionMethod,
|
||||
},
|
||||
logger: defaultLogger,
|
||||
});
|
||||
transformStream.pipe(gcsLoggerStream);
|
||||
await gcsLoggerStream.init();
|
||||
}
|
||||
|
||||
const logBuffer = new WorkerLogBuffer(MAX_LINES_IN_BUFFER);
|
||||
buildLogger.addStream({
|
||||
type: 'raw',
|
||||
stream: logBuffer,
|
||||
reemitErrorEvents: true,
|
||||
level: logger.level(),
|
||||
level: buildLogger.level(),
|
||||
});
|
||||
|
||||
return { logger, stream, logBuffer };
|
||||
return {
|
||||
logger: buildLogger,
|
||||
cleanUp: async () => {
|
||||
await gcsLoggerStream?.cleanUp();
|
||||
},
|
||||
logBuffer,
|
||||
outputStream: transformStream,
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultLogger;
|
||||
@@ -72,9 +72,15 @@ export async function prepareRuntimeEnvironment(
|
||||
return;
|
||||
}
|
||||
|
||||
const currentYarnVersion = (await spawn('yarn', ['--version'], { stdio: 'pipe' })).stdout.trim();
|
||||
const currentPnpmVersion = (await spawn('pnpm', ['--version'], { stdio: 'pipe' })).stdout.trim();
|
||||
const currentBunVersion = (await spawn('bun', ['--version'], { stdio: 'pipe' })).stdout.trim();
|
||||
const currentYarnVersion = (
|
||||
await spawn('yarn', ['--version'], { stdio: 'pipe', cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
const currentPnpmVersion = (
|
||||
await spawn('pnpm', ['--version'], { stdio: 'pipe', cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
const currentBunVersion = (
|
||||
await spawn('bun', ['--version'], { stdio: 'pipe', cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
|
||||
if (builderConfig.node) {
|
||||
const installedNodeVersion = await installNode(ctx, builderConfig.node);
|
||||
@@ -204,7 +210,7 @@ async function installBun({
|
||||
});
|
||||
|
||||
const currentBunVersion = (
|
||||
await spawn('bun', ['--version'], { stdio: 'pipe', env: ctx.env })
|
||||
await spawn('bun', ['--version'], { stdio: 'pipe', env: ctx.env, cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
if (currentBunVersion !== versionToInstall) {
|
||||
throw new Error(
|
||||
@@ -242,7 +248,7 @@ async function installPnpm({
|
||||
env: ctx.env,
|
||||
});
|
||||
const currentPnpmVersion = (
|
||||
await spawn('pnpm', ['--version'], { stdio: 'pipe', env: ctx.env })
|
||||
await spawn('pnpm', ['--version'], { stdio: 'pipe', env: ctx.env, cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
if (currentPnpmVersion !== versionToInstall) {
|
||||
throw new Error(
|
||||
@@ -279,7 +285,7 @@ async function installYarn({
|
||||
env: ctx.env,
|
||||
});
|
||||
const currentYarnVersion = (
|
||||
await spawn('yarn', ['--version'], { stdio: 'pipe', env: ctx.env })
|
||||
await spawn('yarn', ['--version'], { stdio: 'pipe', env: ctx.env, cwd: os.tmpdir() })
|
||||
).stdout.trim();
|
||||
if (currentYarnVersion !== versionToInstall) {
|
||||
throw new Error(
|
||||
|
||||
@@ -31,7 +31,6 @@ import logger, { createBuildLoggerWithSecretsFilter } from './logger';
|
||||
import sentry from './sentry';
|
||||
import State from './state';
|
||||
import { WebSocketServer } from './utils/WebSocketServer';
|
||||
import { LoggerStream } from './utils/logger';
|
||||
import { turtleFetch } from './utils/turtleFetch';
|
||||
|
||||
export const HANGING_WORKER_CHECK_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
@@ -42,7 +41,7 @@ export default class BuildService {
|
||||
private readonly state: State = new State();
|
||||
private ws: WebSocketServer<LauncherMessage.Message, WorkerMessage.Message> | null = null;
|
||||
private shouldCloseWorker = false;
|
||||
private loggerStream?: LoggerStream;
|
||||
private logsCleanUp?: () => Promise<void>;
|
||||
private _startBuildTime?: number;
|
||||
private buildContext?: BuildContext<Job>;
|
||||
|
||||
@@ -121,7 +120,7 @@ export default class BuildService {
|
||||
const isSocketClosed: boolean = !this.ws;
|
||||
// wait 5 seconds to make sure all logs are flushed
|
||||
await setTimeoutAsync(5 * 1000);
|
||||
await this.loggerStream?.cleanUp();
|
||||
await this.logsCleanUp?.();
|
||||
if (this.ws) {
|
||||
logger.info('Send build result - error');
|
||||
this.ws.send({
|
||||
@@ -142,7 +141,7 @@ export default class BuildService {
|
||||
buildArtifactsName: artifacts.BUILD_ARTIFACTS ?? null,
|
||||
});
|
||||
const isSocketClosed: boolean = !this.ws;
|
||||
await this.loggerStream?.cleanUp();
|
||||
await this.logsCleanUp?.();
|
||||
if (this.ws) {
|
||||
logger.info('Send build result - success');
|
||||
this.ws.send({
|
||||
@@ -194,7 +193,7 @@ export default class BuildService {
|
||||
}
|
||||
|
||||
const isSocketClosed: boolean = !this.ws;
|
||||
await this.loggerStream?.cleanUp();
|
||||
await this.logsCleanUp?.();
|
||||
if (this.ws) {
|
||||
logger.info('Send build result - aborted');
|
||||
this.ws.send({
|
||||
@@ -280,10 +279,10 @@ export default class BuildService {
|
||||
try {
|
||||
const {
|
||||
logger: buildLogger,
|
||||
stream,
|
||||
cleanUp,
|
||||
logBuffer,
|
||||
} = await createBuildLoggerWithSecretsFilter(job.secrets?.environmentSecrets);
|
||||
this.loggerStream = stream;
|
||||
this.logsCleanUp = cleanUp;
|
||||
|
||||
const analytics = new Analytics(initiatingUserId, metadata?.trackingContext ?? {});
|
||||
|
||||
@@ -330,37 +329,32 @@ export default class BuildService {
|
||||
},
|
||||
});
|
||||
|
||||
if (err.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
|
||||
let rawErrorMessage: string;
|
||||
if (maybeRawError instanceof Error) {
|
||||
rawErrorMessage = maybeRawError.message;
|
||||
} else {
|
||||
try {
|
||||
rawErrorMessage = JSON.stringify(maybeRawError);
|
||||
} catch {
|
||||
rawErrorMessage = String(maybeRawError);
|
||||
}
|
||||
const robotAccessToken = job.secrets?.robotAccessToken;
|
||||
if (robotAccessToken && err.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
|
||||
let rawErrorMessage: string = '';
|
||||
if (maybeRawError.stderr) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stderr);
|
||||
}
|
||||
if (maybeRawError.stdout) {
|
||||
rawErrorMessage += '\n' + getLastNLines(100, maybeRawError.stdout);
|
||||
}
|
||||
|
||||
const robotAccessToken = job.secrets?.robotAccessToken;
|
||||
if (robotAccessToken) {
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
await turtleFetch(
|
||||
new URL('turtle-builds/error-logs', config.wwwApiV2BaseUrl).toString(),
|
||||
'POST',
|
||||
{
|
||||
json: {
|
||||
buildId: this.buildId,
|
||||
message: rawErrorMessage,
|
||||
buildPhase: err.buildPhase ?? null,
|
||||
errorCode: err.errorCode,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
shouldThrowOnNotOk: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await this.finishError(err, maybeArtifacts);
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { GCSLoggerStream } from '@expo/build-tools';
|
||||
import { BuildPhase } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Transform, Writable } from 'stream';
|
||||
|
||||
export interface LoggerStream extends Writable {
|
||||
writable: boolean;
|
||||
init(): Promise<string>;
|
||||
cleanUp(): Promise<void>;
|
||||
write(rec: any): boolean;
|
||||
}
|
||||
|
||||
export interface BuildLogger {
|
||||
logger: bunyan;
|
||||
stream?: LoggerStream;
|
||||
}
|
||||
|
||||
export function makeKeyForBuildLogs(prefix: string): string {
|
||||
return `${prefix}/${Date.now()}-${randomUUID()}.txt`;
|
||||
}
|
||||
|
||||
async function createBuildLogger(logger: bunyan, stream?: NodeJS.WritableStream): Promise<bunyan> {
|
||||
const fields =
|
||||
'phase' in (logger.fields as Record<string, any>) ? {} : { phase: BuildPhase.UNKNOWN };
|
||||
|
||||
const childLogger = logger.child(fields);
|
||||
if (stream) {
|
||||
childLogger.addStream({
|
||||
type: 'raw',
|
||||
stream,
|
||||
reemitErrorEvents: true,
|
||||
level: logger.level(),
|
||||
});
|
||||
}
|
||||
|
||||
return childLogger;
|
||||
}
|
||||
|
||||
export async function createGCSBuildLogger({
|
||||
transformStream,
|
||||
...config
|
||||
}: GCSLoggerStream.Config & { transformStream?: Transform }): Promise<BuildLogger> {
|
||||
if (config.uploadMethod) {
|
||||
const stream = new GCSLoggerStream(config);
|
||||
transformStream?.pipe(stream);
|
||||
await stream.init();
|
||||
const logger = await createBuildLogger(config.logger, transformStream ?? stream);
|
||||
return {
|
||||
logger,
|
||||
stream,
|
||||
};
|
||||
} else {
|
||||
return { logger: await createBuildLogger(config.logger) };
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -6,7 +6,7 @@ SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
pushd $SCRIPTS_DIR >/dev/null 2>&1
|
||||
# Install modules
|
||||
yarn --silent
|
||||
yarn
|
||||
# Run script
|
||||
yarn --silent $@
|
||||
yarn $@
|
||||
popd >/dev/null 2>&1
|
||||
@@ -12,9 +12,11 @@ ROOT_DIR="$( cd "$SCRIPTS_DIR"/.. && pwd )"
|
||||
# to @expo/eas-build-job dependency, so the update-local-plugin step is no longer needed.
|
||||
# $SCRIPTS_DIR/bin/run update-local-plugin
|
||||
|
||||
next_version_bump=$($SCRIPTS_DIR/bin/run next-version)
|
||||
next_version_bump=`(cd $SCRIPTS_DIR; node --no-warnings=ExperimentalWarning --loader ts-node/esm src/nextVersion.ts)`
|
||||
next_version=${1:-$next_version_bump}
|
||||
|
||||
echo "next_version = ${next_version}"
|
||||
|
||||
if [[ "$GITHUB_USER" == "Expo CI" && "$GITHUB_EMAIL" == "[email protected]" && "$INPUT_DRY_RUN" != "true" ]]; then
|
||||
echo "Releasing with version $next_version"
|
||||
lerna version --yes --exact "$next_version"
|
||||
|
||||
Generated
+327
-187
@@ -1,238 +1,378 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"@cspotcode/source-map-support@^0.8.0":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||
"@cspotcode/source-map-support@npm:^0.8.0":
|
||||
version: 0.8.1
|
||||
resolution: "@cspotcode/source-map-support@npm:0.8.1"
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
"@jridgewell/trace-mapping": "npm:0.3.9"
|
||||
checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
"@jridgewell/resolve-uri@npm:^3.0.3":
|
||||
version: 3.1.0
|
||||
resolution: "@jridgewell/resolve-uri@npm:3.1.0"
|
||||
checksum: 10c0/78055e2526108331126366572045355051a930f017d1904a4f753d3f4acee8d92a14854948095626f6163cffc24ea4e3efa30637417bb866b84743dec7ef6fd9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
"@jridgewell/sourcemap-codec@npm:^1.4.10":
|
||||
version: 1.4.14
|
||||
resolution: "@jridgewell/sourcemap-codec@npm:1.4.14"
|
||||
checksum: 10c0/3fbaff1387c1338b097eeb6ff92890d7838f7de0dde259e4983763b44540bfd5ca6a1f7644dc8ad003a57f7e80670d5b96a8402f1386ba9aee074743ae9bad51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/[email protected]":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||
"@jridgewell/trace-mapping@npm:0.3.9":
|
||||
version: 0.3.9
|
||||
resolution: "@jridgewell/trace-mapping@npm:0.3.9"
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/resolve-uri": "npm:^3.0.3"
|
||||
"@jridgewell/sourcemap-codec": "npm:^1.4.10"
|
||||
checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
|
||||
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
|
||||
"@tsconfig/node10@npm:^1.0.7":
|
||||
version: 1.0.8
|
||||
resolution: "@tsconfig/node10@npm:1.0.8"
|
||||
checksum: 10c0/d400f7b5c02acd74620f892c0f41cea39e7c1b5f7f272ad6f127f4b1fba23346b2d8e30d272731a733675494145f6aa74f9faf050390c034c7c553123ab979b3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
|
||||
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
|
||||
"@tsconfig/node12@npm:^1.0.7":
|
||||
version: 1.0.9
|
||||
resolution: "@tsconfig/node12@npm:1.0.9"
|
||||
checksum: 10c0/fc1fb68a89d8a641953036d23d95fe68f69f74d37a499db20791b09543ad23afe7ae9ee0840eea92dd470bdcba69eef6f1ed3fe90ba64d763bcd3f738e364597
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
|
||||
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
|
||||
"@tsconfig/node14@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "@tsconfig/node14@npm:1.0.1"
|
||||
checksum: 10c0/abd4e27d9ad712e1e229716a3dbf35d5cbb580d624a82d67414e7606cefd85d502e58800a2ab930d46a428fcfcb199436283b1a88e47d738ca1a5f7fd022ee74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
|
||||
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
|
||||
"@tsconfig/node16@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@tsconfig/node16@npm:1.0.2"
|
||||
checksum: 10c0/d402706562444a173d48810d13fdf866c78f1b876ed8962eeac6c7cddf4e29e8aaa06dc28093219e3e9eb6316799cf4d9a7acba62c6a4e215ee0c94d83f9081f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/[email protected]":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
|
||||
integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
|
||||
"@types/dateformat@npm:3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "@types/dateformat@npm:3.0.1"
|
||||
checksum: 10c0/f0cbd170987ede1fb4b541f1471de7eb722577b391ba3209df6e6840f32cb3ec2be8a8a5e00ff364834429d16bc341472087d91a960b903f331f5c44073af9a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/[email protected]":
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45"
|
||||
integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==
|
||||
"@types/fs-extra@npm:11.0.4":
|
||||
version: 11.0.4
|
||||
resolution: "@types/fs-extra@npm:11.0.4"
|
||||
dependencies:
|
||||
"@types/jsonfile" "*"
|
||||
"@types/node" "*"
|
||||
"@types/jsonfile": "npm:*"
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/9e34f9b24ea464f3c0b18c3f8a82aefc36dc524cc720fc2b886e5465abc66486ff4e439ea3fb2c0acebf91f6d3f74e514f9983b1f02d4243706bdbb7511796ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsonfile@*":
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702"
|
||||
integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==
|
||||
"@types/jsonfile@npm:*":
|
||||
version: 6.1.4
|
||||
resolution: "@types/jsonfile@npm:6.1.4"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/b12d068b021e4078f6ac4441353965769be87acf15326173e2aea9f3bf8ead41bd0ad29421df5bbeb0123ec3fc02eb0a734481d52903704a1454a1845896b9eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/[email protected]":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||
"@types/lodash-es@npm:4.17.12":
|
||||
version: 4.17.12
|
||||
resolution: "@types/lodash-es@npm:4.17.12"
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
"@types/lodash": "npm:*"
|
||||
checksum: 10c0/5d12d2cede07f07ab067541371ed1b838a33edb3c35cb81b73284e93c6fd0c4bbeaefee984e69294bffb53f62d7272c5d679fdba8e595ff71e11d00f2601dde0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.182"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
|
||||
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
|
||||
"@types/lodash@npm:*":
|
||||
version: 4.14.182
|
||||
resolution: "@types/lodash@npm:4.14.182"
|
||||
checksum: 10c0/d6bd4789dfb3be631d5e3277e6a1be5becb21440f3364f5d15b982c2e6b6bb1f8048d46fc5bff5ef0f90bebaf4d07c49b2919ba369d07af72d3beb3fea70c61a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@*":
|
||||
version "16.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5"
|
||||
integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==
|
||||
"@types/node@npm:*":
|
||||
version: 16.10.3
|
||||
resolution: "@types/node@npm:16.10.3"
|
||||
checksum: 10c0/8d6a81dc5b5d5dd2a7c7025b378f9d1c7a212bb1ebbf048070988d7b2f66b51278dc462090403a9a882dc305d3086ce61979a3f2ff2c4930b62831494c8f6abd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/[email protected]":
|
||||
version "20.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
|
||||
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
|
||||
"@types/node@npm:20.11.0":
|
||||
version: 20.11.0
|
||||
resolution: "@types/node@npm:20.11.0"
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
undici-types: "npm:~5.26.4"
|
||||
checksum: 10c0/560aa850dfccb83326f9cba125459f6c3fb0c71ec78f22c61e4d248f1df78bd25fd6792cef573dfbdc49c882f8e38bb1a82ca87e0e28ff2513629c704c2b02af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/[email protected]":
|
||||
version "7.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
|
||||
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
|
||||
"@types/semver@npm:7.5.6":
|
||||
version: 7.5.6
|
||||
resolution: "@types/semver@npm:7.5.6"
|
||||
checksum: 10c0/196dc32db5f68cbcde2e6a42bb4aa5cbb100fa2b7bd9c8c82faaaf3e03fbe063e205dbb4f03c7cdf53da2edb70a0d34c9f2e601b54281b377eb8dc1743226acd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
"acorn-walk@npm:^8.1.1":
|
||||
version: 8.2.0
|
||||
resolution: "acorn-walk@npm:8.2.0"
|
||||
checksum: 10c0/dbe92f5b2452c93e960c5594e666dd1fae141b965ff2cb4a1e1d0381e3e4db4274c5ce4ffa3d681a86ca2a8d4e29d5efc0670a08e23fd2800051ea387df56ca2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
acorn@^8.4.1:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
|
||||
integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
|
||||
"acorn@npm:^8.4.1":
|
||||
version: 8.5.0
|
||||
resolution: "acorn@npm:8.5.0"
|
||||
bin:
|
||||
acorn: bin/acorn
|
||||
checksum: 10c0/a0cd0009d215df0c5a6c69ea40a5e784114f1b89d5649d663264ccbb855d4c6b6da6936fed607078b7d681bd8d8f2e730dad25931edbdef709bc4b520c0caa80
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
"arg@npm:^4.1.0":
|
||||
version: 4.1.3
|
||||
resolution: "arg@npm:4.1.3"
|
||||
checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"
|
||||
integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==
|
||||
"chalk@npm:5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "chalk@npm:5.2.0"
|
||||
checksum: 10c0/8a519b35c239f96e041b7f1ed8fdd79d3ca2332a8366cb957378b8a1b8a4cdfb740d19628e8bf74654d4c0917aa10cf39c20752e177a1304eac29a1168a740e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
"create-require@npm:^1.1.0":
|
||||
version: 1.1.1
|
||||
resolution: "create-require@npm:1.1.1"
|
||||
checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
|
||||
integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
|
||||
"dateformat@npm:4.5.1":
|
||||
version: 4.5.1
|
||||
resolution: "dateformat@npm:4.5.1"
|
||||
checksum: 10c0/b8a0729386254f8e1633b552f156160777302f91cd81bba3dc91641b80ce2ea7f330e2980e95ee460e079fb4d7d1b821ef8562f38dc2b461c7101db9b503f504
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
"diff@npm:^4.0.1":
|
||||
version: 4.0.2
|
||||
resolution: "diff@npm:4.0.2"
|
||||
checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
|
||||
integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==
|
||||
"fs-extra@npm:11.2.0":
|
||||
version: 11.2.0
|
||||
resolution: "fs-extra@npm:11.2.0"
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
graceful-fs: "npm:^4.2.0"
|
||||
jsonfile: "npm:^6.0.1"
|
||||
universalify: "npm:^2.0.0"
|
||||
checksum: 10c0/d77a9a9efe60532d2e790e938c81a02c1b24904ef7a3efb3990b835514465ba720e99a6ea56fd5e2db53b4695319b644d76d5a0e9988a2beef80aa7b1da63398
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||
"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0":
|
||||
version: 4.2.8
|
||||
resolution: "graceful-fs@npm:4.2.8"
|
||||
checksum: 10c0/68365485460f7d2e95c05c1b7aeee935349f3b7776488d5bd95a45d8a45bd4977442e88cbbdb4ea01bc72f49f01f75d83f049069774ac8cc4328af4bcff1c542
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
"jsonfile@npm:^6.0.1":
|
||||
version: 6.1.0
|
||||
resolution: "jsonfile@npm:6.1.0"
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
graceful-fs: "npm:^4.1.6"
|
||||
universalify: "npm:^2.0.0"
|
||||
dependenciesMeta:
|
||||
graceful-fs:
|
||||
optional: true
|
||||
checksum: 10c0/4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
"lodash-es@npm:4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash-es@npm:4.17.21"
|
||||
checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
"lru-cache@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "lru-cache@npm:6.0.0"
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
yallist: "npm:^4.0.0"
|
||||
checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
"make-error@npm:^1.1.1":
|
||||
version: 1.3.6
|
||||
resolution: "make-error@npm:1.3.6"
|
||||
checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-3.0.4.tgz#b8a1539e5e05c6ea9e93f15c0bad1d54ce890406"
|
||||
integrity sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA==
|
||||
"marked@npm:3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "marked@npm:3.0.4"
|
||||
bin:
|
||||
marked: bin/marked
|
||||
checksum: 10c0/2c31fab4d3a65a84895ad26e25b238baf93453ff8344df83bf79f4940636fde5bd744747261eba468f773f21124d7da10b2cf40d4101dbe71aa885f04e61db7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
"scripts@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "scripts@workspace:."
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
"@types/dateformat": "npm:3.0.1"
|
||||
"@types/fs-extra": "npm:11.0.4"
|
||||
"@types/lodash-es": "npm:4.17.12"
|
||||
"@types/node": "npm:20.11.0"
|
||||
"@types/semver": "npm:7.5.6"
|
||||
chalk: "npm:5.2.0"
|
||||
dateformat: "npm:4.5.1"
|
||||
fs-extra: "npm:11.2.0"
|
||||
lodash-es: "npm:4.17.21"
|
||||
marked: "npm:3.0.4"
|
||||
semver: "npm:7.5.4"
|
||||
ts-node: "npm:10.9.2"
|
||||
tslib: "npm:2.6.2"
|
||||
typescript: "npm:5.3.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
[email protected]:
|
||||
version "10.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
|
||||
"semver@npm:7.5.4":
|
||||
version: 7.5.4
|
||||
resolution: "semver@npm:7.5.4"
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "^0.8.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
lru-cache: "npm:^6.0.0"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: 10c0/5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
ts[email protected].2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
"ts-node@npm:10.9.2":
|
||||
version: 10.9.2
|
||||
resolution: "ts-node@npm:10.9.2"
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support": "npm:^0.8.0"
|
||||
"@tsconfig/node10": "npm:^1.0.7"
|
||||
"@tsconfig/node12": "npm:^1.0.7"
|
||||
"@tsconfig/node14": "npm:^1.0.0"
|
||||
"@tsconfig/node16": "npm:^1.0.2"
|
||||
acorn: "npm:^8.4.1"
|
||||
acorn-walk: "npm:^8.1.1"
|
||||
arg: "npm:^4.1.0"
|
||||
create-require: "npm:^1.1.0"
|
||||
diff: "npm:^4.0.1"
|
||||
make-error: "npm:^1.1.1"
|
||||
v8-compile-cache-lib: "npm:^3.0.1"
|
||||
yn: "npm:3.1.1"
|
||||
peerDependencies:
|
||||
"@swc/core": ">=1.2.50"
|
||||
"@swc/wasm": ">=1.2.50"
|
||||
"@types/node": "*"
|
||||
typescript: ">=2.7"
|
||||
peerDependenciesMeta:
|
||||
"@swc/core":
|
||||
optional: true
|
||||
"@swc/wasm":
|
||||
optional: true
|
||||
bin:
|
||||
ts-node: dist/bin.js
|
||||
ts-node-cwd: dist/bin-cwd.js
|
||||
ts-node-esm: dist/bin-esm.js
|
||||
ts-node-script: dist/bin-script.js
|
||||
ts-node-transpile-only: dist/bin-transpile.js
|
||||
ts-script: dist/bin-script-deprecated.js
|
||||
checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
[email protected]:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
"tslib@npm:2.6.2":
|
||||
version: 2.6.2
|
||||
resolution: "tslib@npm:2.6.2"
|
||||
checksum: 10c0/e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
"typescript@npm:5.3.3":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@npm:5.3.3"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10c0/e33cef99d82573624fc0f854a2980322714986bc35b9cb4d1ce736ed182aeab78e2cb32b385efa493b2a976ef52c53e20d6c6918312353a91850e2b76f1ea44f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
"typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>::version=5.3.3&hash=e012d7"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10c0/1d0a5f4ce496c42caa9a30e659c467c5686eae15d54b027ee7866744952547f1be1262f2d40de911618c242b510029d51d43ff605dba8fb740ec85ca2d3f9500
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
"undici-types@npm:~5.26.4":
|
||||
version: 5.26.5
|
||||
resolution: "undici-types@npm:5.26.5"
|
||||
checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
"universalify@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "universalify@npm:2.0.0"
|
||||
checksum: 10c0/07092b9f46df61b823d8ab5e57f0ee5120c178b39609a95e4a15a98c42f6b0b8e834e66fbb47ff92831786193be42f1fd36347169b88ce8639d0f9670af24a71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
"v8-compile-cache-lib@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "v8-compile-cache-lib@npm:3.0.1"
|
||||
checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "yallist@npm:4.0.0"
|
||||
checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yn@npm:3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "yn@npm:3.1.1"
|
||||
checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443
|
||||
languageName: node
|
||||
linkType: hard
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
exec corepack yarn "$@"
|
||||
Reference in new issue
Block a user