mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba41af60b9 | ||
|
|
537e3be30f | ||
|
|
f81efbacde | ||
|
|
06c6a8416d | ||
|
|
e9ebc41413 | ||
|
|
9313ef11ca | ||
|
|
fd80532a7e | ||
|
|
f20ccebcad | ||
|
|
e8602f7062 | ||
|
|
d14c0b196b | ||
|
|
c24b9a037a | ||
|
|
c3c3f087ac | ||
|
|
ac761edb5c | ||
|
|
78c30acbc9 | ||
|
|
a716925ed7 | ||
|
|
e579441d5b | ||
|
|
4f5831305b | ||
|
|
98db228cc8 | ||
|
|
5c762d300e | ||
|
|
2a6bee7f5a | ||
|
|
4d365e0355 | ||
|
|
48aa2ccde2 | ||
|
|
bdc04e3e6b | ||
|
|
935491f967 | ||
|
|
031198d1aa | ||
|
|
ac60d159f2 | ||
|
|
fc09fe4a34 |
No files matched your search
@@ -2,7 +2,7 @@ commentConfiguration:
|
||||
preamble: 'Subscribed to pull request'
|
||||
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
|
||||
rules:
|
||||
- patterns: ['**/*']
|
||||
- patterns: ['packages/eas-cli/**']
|
||||
mentions: ['douglowder']
|
||||
- patterns: ['packages/eas-cli/src/update/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: License
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 1-12/3 *' # Run every three months
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/license.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update BSL change date
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- name: Check last commit date
|
||||
run: |
|
||||
# The date in the license
|
||||
change_date=$(sed --quiet --regexp-extended 's/^Change Date:\s+([0-9]{4}-[0-9]{2}-[0-9]{2})/\1/p' < LICENSE-BUSL)
|
||||
[ -z "$change_date" ] && exit 1
|
||||
|
||||
new_change_date=$(date --date "+3 years" --iso-8601)
|
||||
|
||||
if [ "$(date --date "$new_change_date" +%s)" -gt "$(date --date "$change_date" +%s)" ]; then
|
||||
sed --in-place "s/$change_date/$new_change_date/" LICENSE-BUSL
|
||||
git config user.name 'Expo Bot'
|
||||
git config user.email [email protected]
|
||||
git add LICENSE-BUSL
|
||||
git commit -m "[license] Update BSL Change Date to $new_change_date"
|
||||
if [ "$(git branch --show-current)" = main ]; then
|
||||
git push
|
||||
fi
|
||||
fi
|
||||
+29
-2
@@ -8,13 +8,38 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add diff output to `eas env:pull`. ([#4036](https://github.com/expo/eas-cli/pull/4036) by [@khamilowicz](https://github.com/khamilowicz))
|
||||
|
||||
- [eas-cli] Support drill-down from event lists in observe:session. ([#3987](https://github.com/expo/eas-cli/pull/3987) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [21.0.2](https://github.com/expo/eas-cli/releases/tag/v21.0.2) - 2026-07-17
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Remove temporary working directories after `eas build:inspect` copies inspect output, avoiding leftover disk usage after failed builds. ([#3981](https://github.com/expo/eas-cli/pull/3981) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- [eas-cli] Bump `tmp` to `0.2.7` to resolve [Dependabot alert 423](https://github.com/expo/eas-cli/security/dependabot/423). ([#3965](https://github.com/expo/eas-cli/pull/3965) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- [worker] Bump `ws` to patched releases to resolve [Dependabot alert 462](https://github.com/expo/eas-cli/security/dependabot/462). ([#3963](https://github.com/expo/eas-cli/pull/3963) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Bump `lodash` and `lodash-es` to `4.18.1` across workspaces and upgrade GraphQL Code Generator to remove the remaining vulnerable transitive copy for Dependabot alert 354. ([#4009](https://github.com/expo/eas-cli/pull/4009) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- [eas-cli] Bump `sigstore` to `4.1.1` to resolve [Dependabot alert 475](https://github.com/expo/eas-cli/security/dependabot/475). ([#3961](https://github.com/expo/eas-cli/pull/3961) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- [eas-cli] Bump `axios` to patched releases to resolve [Dependabot alert 439](https://github.com/expo/eas-cli/security/dependabot/439). ([#3964](https://github.com/expo/eas-cli/pull/3964) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- [eas-cli] Bump `handlebars` to `4.7.9` to resolve [Dependabot alert 334](https://github.com/expo/eas-cli/security/dependabot/334). ([#3958](https://github.com/expo/eas-cli/pull/3958) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [21.0.1](https://github.com/expo/eas-cli/releases/tag/v21.0.1) - 2026-07-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `eas project:delete` command to delete a project — the current directory's project by default, or any project passed as an argument (`@account/slug` or ID). Confirmation requires the project's full name, typed at the interactive prompt or passed via `--dangerously-confirm-deletion` (required in non-interactive mode). Deletion requires sudo mode: the command prompts for your password (and OTP) to upgrade the session when the server requires it. Deletion was previously only possible from the website. ([#4012](https://github.com/expo/eas-cli/pull/4012) by [@brentvatne](https://github.com/brentvatne))
|
||||
- [eas-cli] Add `eas browse` to open the project page on expo.dev in a web browser, with an optional page argument (e.g. `eas browse build`, `eas browse hosting`) for project subpages and a `--no-browser` flag to print the URL. `eas open` is now an alias of `eas browse`. ([#4003](https://github.com/expo/eas-cli/pull/4003) by [@jonsamp](https://github.com/jonsamp))
|
||||
- [eas-cli] Add `--dev-domain` flag to `eas deploy` to choose the project's preview URL (`<name>.expo.app`) without prompting, allowing first-time deploys with `--non-interactive`. ([#4004](https://github.com/expo/eas-cli/pull/4004) by [@brentvatne](https://github.com/brentvatne))
|
||||
- [eas-cli] Add `-p` as an alias for `--platform` in `eas simulator:start`. ([#3902](https://github.com/expo/eas-cli/pull/3902) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- [eas-cli][build-tools][local-build-plugin][worker] Bump `tar` to `7.5.19`. ([#3971](https://github.com/expo/eas-cli/pull/3971) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
@@ -45,6 +70,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade Expo plist dependencies to use `@xmldom/xmldom` `0.8.13` without a root resolution, addressing Dependabot alert 372. ([#4010](https://github.com/expo/eas-cli/pull/4010) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- [eas-cli] add agent detection to analytics events. ([#3983](https://github.com/expo/eas-cli/pull/3983) by [@davidmokos](https://github.com/davidmokos))
|
||||
- Bump `glob` 10.x to a patched version in the root lockfile. ([#3979](https://github.com/expo/eas-cli/pull/3979) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- [build-tools] Bump `jws` to a patched version in the root lockfile. ([#3978](https://github.com/expo/eas-cli/pull/3978) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
@@ -61,6 +87,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Fix Free plan users being warned they had reached their build limit while still well below it. Overage warnings are now shown only for paid plans with an actual billable overage cost. ([#3882](https://github.com/expo/eas-cli/pull/3882) by [@sarahlane8](https://github.com/sarahlane8))
|
||||
- [eas-cli] Count only genuine per-worker build overages in the pre-build usage warning, so the reported number of builds beyond included credits is accurate. ([#3932](https://github.com/expo/eas-cli/pull/3932) by [@sarahlane8](https://github.com/sarahlane8))
|
||||
- [eas-cli] Select correct tvOS build target for non-interactive Apple builds when EXPO_TV env variable is set. ([#3907](https://github.com/expo/eas-cli/pull/3907) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that you d
|
||||
to access the functionality of and directly benefit from the
|
||||
functionality of the Licensed Work.
|
||||
|
||||
Change Date: 2028-04-01
|
||||
Change Date: 2029-07-15
|
||||
|
||||
Change License: MIT
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -39,18 +39,17 @@
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/downloader": "21.0.0",
|
||||
"@expo/eas-build-job": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "21.0.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/plist": "^0.3.5",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "21.0.1",
|
||||
"@expo/template-file": "21.0.0",
|
||||
"@expo/steps": "21.0.2",
|
||||
"@expo/template-file": "21.0.2",
|
||||
"@expo/turtle-spawn": "21.0.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
"@google-cloud/storage": "^7.11.2",
|
||||
"@ngrok/ngrok": "1.7.0",
|
||||
"@sentry/node": "7.77.0",
|
||||
"@urql/core": "^6.0.1",
|
||||
@@ -61,7 +60,7 @@
|
||||
"gql.tada": "^1.8.13",
|
||||
"joi": "^17.13.1",
|
||||
"jose": "5.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.18.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
|
||||
@@ -59,8 +59,15 @@ public struct FirstFrameWallClock: Codable, Sendable {
|
||||
public let iso8601: String
|
||||
}
|
||||
|
||||
public enum RecordingFinalizationStage: Sendable {
|
||||
case captureStopped
|
||||
case videoSaved
|
||||
}
|
||||
|
||||
public struct RecordingManifest: Codable, Sendable {
|
||||
public let firstFrameWallClock: FirstFrameWallClock
|
||||
public let width: Int
|
||||
public let height: Int
|
||||
public let hlsVersion: Int?
|
||||
public let hlsTargetDurationSeconds: Int?
|
||||
public let hlsMediaSequence: Int?
|
||||
|
||||
+5
-1
@@ -100,7 +100,9 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
|
||||
@discardableResult
|
||||
func writeManifest(
|
||||
configuration: SimulatorRecordingConfiguration,
|
||||
firstFrameWallClock: Date
|
||||
firstFrameWallClock: Date,
|
||||
width: Int,
|
||||
height: Int
|
||||
) throws -> RecordingManifest {
|
||||
try stateQueue.sync {
|
||||
if let firstError {
|
||||
@@ -121,6 +123,8 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
|
||||
unixMs: unixMs(firstFrameWallClock),
|
||||
iso8601: iso8601(firstFrameWallClock)
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
hlsVersion: segmented ? 7 : nil,
|
||||
hlsTargetDurationSeconds: targetDuration,
|
||||
hlsMediaSequence: segmented ? 0 : nil,
|
||||
|
||||
+13
-1
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
|
||||
public final class SimulatorRecorder {
|
||||
public var onSegment: ((SegmentOutput) -> Void)?
|
||||
public var onSimulatorStopped: ((String) -> Void)?
|
||||
public var onFinalizationStage: ((RecordingFinalizationStage) -> Void)?
|
||||
|
||||
private static let callbackStalenessTimeout: TimeInterval = 5
|
||||
private static let firstFrameRewireInterval: TimeInterval = 1
|
||||
@@ -26,6 +27,8 @@ public final class SimulatorRecorder {
|
||||
private var monotonicClock = MonotonicClock()
|
||||
private var firstAcceptedCaptureTime: CMTime?
|
||||
private var firstAcceptedWallClock: Date?
|
||||
private var recordingWidth: Int?
|
||||
private var recordingHeight: Int?
|
||||
private var lastPTS: CMTime?
|
||||
private var lastSeed: UInt32?
|
||||
private var lastFrameCallbackElapsed: TimeInterval?
|
||||
@@ -113,6 +116,7 @@ public final class SimulatorRecorder {
|
||||
displaySource?.stop()
|
||||
displaySource = nil
|
||||
}
|
||||
onFinalizationStage?(.captureStopped)
|
||||
|
||||
return try writerQueue.sync {
|
||||
if let firstError {
|
||||
@@ -139,12 +143,18 @@ public final class SimulatorRecorder {
|
||||
if let error = outputWriter.error {
|
||||
throw error
|
||||
}
|
||||
onFinalizationStage?(.videoSaved)
|
||||
guard let firstAcceptedWallClock else {
|
||||
throw RecorderError.make(25, "Missing first frame wall-clock timestamp")
|
||||
}
|
||||
guard let recordingWidth, let recordingHeight else {
|
||||
throw RecorderError.make(27, "Missing recording dimensions")
|
||||
}
|
||||
return try outputWriter.writeManifest(
|
||||
configuration: configuration,
|
||||
firstFrameWallClock: firstAcceptedWallClock
|
||||
firstFrameWallClock: firstAcceptedWallClock,
|
||||
width: recordingWidth,
|
||||
height: recordingHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -590,6 +600,8 @@ public final class SimulatorRecorder {
|
||||
self.writer = writer
|
||||
self.input = input
|
||||
self.adaptor = adaptor
|
||||
recordingWidth = width
|
||||
recordingHeight = height
|
||||
}
|
||||
|
||||
private func assetWriterSettings(width: Int, height: Int) -> [String: Any] {
|
||||
|
||||
@@ -10,15 +10,15 @@ struct CLIOptions {
|
||||
var codec: RecorderCodec = .h264
|
||||
}
|
||||
|
||||
private var stopRequested: CInt = 0
|
||||
private var requestedStopSignal: CInt = 0
|
||||
private var simulatorStopped: CInt = 0
|
||||
|
||||
private func handleStopSignal(_: CInt) {
|
||||
stopRequested = 1
|
||||
private func handleStopSignal(_ signal: CInt) {
|
||||
requestedStopSignal = signal
|
||||
}
|
||||
|
||||
private func isStopRequested() -> Bool {
|
||||
stopRequested != 0 || simulatorStopped != 0
|
||||
requestedStopSignal != 0 || simulatorStopped != 0
|
||||
}
|
||||
|
||||
func usage() -> String {
|
||||
@@ -112,6 +112,14 @@ do {
|
||||
simulatorStopped = 1
|
||||
fputs("[record-sim] simulator stopped state=\(state); finalizing recording\n", stderr)
|
||||
}
|
||||
recorder.onFinalizationStage = { stage in
|
||||
switch stage {
|
||||
case .captureStopped:
|
||||
fputs("[record-sim] screen capture stopped; saving final video\n", stderr)
|
||||
case .videoSaved:
|
||||
fputs("[record-sim] final video saved; writing recording details\n", stderr)
|
||||
}
|
||||
}
|
||||
recorder.onSegment = { segment in
|
||||
switch segment.kind {
|
||||
case .initialization:
|
||||
@@ -130,6 +138,14 @@ do {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
|
||||
if requestedStopSignal != 0 {
|
||||
let signalName = switch requestedStopSignal {
|
||||
case SIGINT: "SIGINT"
|
||||
case SIGTERM: "SIGTERM"
|
||||
default: "signal \(requestedStopSignal)"
|
||||
}
|
||||
fputs("[record-sim] received \(signalName); finishing recording\n", stderr)
|
||||
}
|
||||
let manifest = try recorder.stop()
|
||||
if let recording = manifest.recording {
|
||||
fputs("[record-sim] done recording=\(recording)\n", stderr)
|
||||
|
||||
@@ -19,12 +19,10 @@ jest.mock('fs-extra');
|
||||
jest.mock('../datadog', () => ({
|
||||
Datadog: {
|
||||
distribution: jest.fn(),
|
||||
log: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const datadogDistributionMock = jest.mocked(Datadog.distribution);
|
||||
const datadogLogMock = jest.mocked(Datadog.log);
|
||||
|
||||
describe('BuildContext', () => {
|
||||
beforeEach(() => {
|
||||
@@ -171,45 +169,6 @@ describe('BuildContext', () => {
|
||||
expect(ctx.env.EXISTING_ENV).toBe('new-value');
|
||||
expect(ctx.env.REMOVED_ENV).toBeUndefined();
|
||||
expect(ctx.env.NEW_ENV).toBe('new-env-value');
|
||||
const logMessage = datadogLogMock.mock.calls[0][0];
|
||||
expect(logMessage).toContain('BuildContext.updateEnv merge order produced different env');
|
||||
expect(logMessage).toContain('different_env_names=');
|
||||
expect(logMessage).toContain('EXISTING_ENV');
|
||||
expect(logMessage).toContain('REMOVED_ENV');
|
||||
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('old-value');
|
||||
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('new-value');
|
||||
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('new-env-value');
|
||||
});
|
||||
|
||||
it('logs when updateEnv merge order produces the same env without values', async () => {
|
||||
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
secrets: {},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
EXISTING_ENV: 'same-value',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.updateEnv({
|
||||
EXISTING_ENV: 'same-value',
|
||||
UNSET_ENV: undefined,
|
||||
});
|
||||
|
||||
expect(datadogLogMock).toHaveBeenCalledWith(
|
||||
'BuildContext.updateEnv merge order produced same env'
|
||||
);
|
||||
expect(JSON.stringify(datadogLogMock.mock.calls)).not.toContain('same-value');
|
||||
});
|
||||
|
||||
it('emits build phase duration metrics for successful build phases', async () => {
|
||||
|
||||
@@ -72,41 +72,6 @@ export interface BuildContextOptions {
|
||||
|
||||
export class SkipNativeBuildError extends Error {}
|
||||
|
||||
function logEnvComparison({
|
||||
oldMergeOrderEnv,
|
||||
newMergeOrderEnv,
|
||||
}: {
|
||||
oldMergeOrderEnv: Env;
|
||||
newMergeOrderEnv: Env;
|
||||
}): void {
|
||||
const envNames = new Set([...Object.keys(oldMergeOrderEnv), ...Object.keys(newMergeOrderEnv)]);
|
||||
const sameEnvNames: string[] = [];
|
||||
const differentEnvNames: string[] = [];
|
||||
|
||||
for (const key of envNames) {
|
||||
const hasOldValue = key in oldMergeOrderEnv;
|
||||
const hasNewValue = key in newMergeOrderEnv;
|
||||
if (hasOldValue === hasNewValue && oldMergeOrderEnv[key] === newMergeOrderEnv[key]) {
|
||||
sameEnvNames.push(key);
|
||||
} else {
|
||||
differentEnvNames.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (differentEnvNames.length === 0) {
|
||||
Datadog.log('BuildContext.updateEnv merge order produced same env');
|
||||
return;
|
||||
}
|
||||
|
||||
Datadog.log(
|
||||
`BuildContext.updateEnv merge order produced different env: compared_env_count=${
|
||||
envNames.size
|
||||
} different_env_count=${differentEnvNames.length} different_env_names=${differentEnvNames.join(
|
||||
','
|
||||
)} same_env_count=${sameEnvNames.length} same_env_names=${sameEnvNames.join(',')}`
|
||||
);
|
||||
}
|
||||
|
||||
export class BuildContext<TJob extends Job = Job> {
|
||||
public readonly workingdir: string;
|
||||
public logger: bunyan;
|
||||
@@ -283,23 +248,11 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
);
|
||||
}
|
||||
|
||||
const oldMergeOrderEnv: Env = {
|
||||
...env,
|
||||
...this._env,
|
||||
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
||||
};
|
||||
const newMergeOrderEnv: Env = {
|
||||
this._env = {
|
||||
...this._env,
|
||||
...env,
|
||||
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
||||
};
|
||||
|
||||
logEnvComparison({
|
||||
oldMergeOrderEnv,
|
||||
newMergeOrderEnv,
|
||||
});
|
||||
|
||||
this._env = newMergeOrderEnv;
|
||||
this._env.PATH = this._env.PATH
|
||||
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
|
||||
: this.buildExecutablesDirectory;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Readable, Writable, pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import GCS from './client';
|
||||
import { GCS } from './client';
|
||||
|
||||
type PromiseResolveFn = (value?: void | PromiseLike<void> | undefined) => void;
|
||||
|
||||
@@ -41,7 +41,7 @@ class GCSLoggerStream extends Writable {
|
||||
}
|
||||
|
||||
private findNormalizedHeader(name: string): string | null {
|
||||
if (!this.uploadMethod || 'client' in this.uploadMethod) {
|
||||
if (!this.uploadMethod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -165,25 +165,10 @@ class GCSLoggerStream extends Writable {
|
||||
);
|
||||
};
|
||||
|
||||
if ('signedUrl' in this.uploadMethod) {
|
||||
return await GCS.uploadWithSignedUrl({
|
||||
signedUrl: this.uploadMethod.signedUrl,
|
||||
srcGeneratorAsync,
|
||||
});
|
||||
}
|
||||
|
||||
const { Location } = await this.uploadMethod.client.uploadFile({
|
||||
key: this.uploadMethod.key,
|
||||
src: await srcGeneratorAsync(),
|
||||
streamOptions: {
|
||||
metadata: {
|
||||
contentType: 'text/plain;charset=utf-8',
|
||||
...(this.compress !== null ? { contentEncoding: this.compress } : {}),
|
||||
customTime: this.uploadMethod.customTime,
|
||||
},
|
||||
},
|
||||
return await GCS.uploadWithSignedUrl({
|
||||
signedUrl: this.uploadMethod.signedUrl,
|
||||
srcGeneratorAsync,
|
||||
});
|
||||
return Location;
|
||||
}
|
||||
|
||||
private async createCompressedStream(src: Readable): Promise<Readable> {
|
||||
@@ -223,9 +208,9 @@ namespace GCSLoggerStream {
|
||||
compress?: CompressionMethod;
|
||||
}
|
||||
|
||||
export type UploadMethod =
|
||||
| { client: GCS; key: string; customTime: Date | null }
|
||||
| { signedUrl: GCS.SignedUrl };
|
||||
export type UploadMethod = {
|
||||
signedUrl: GCS.SignedUrl;
|
||||
};
|
||||
|
||||
export interface Config {
|
||||
logger: bunyan;
|
||||
|
||||
+34
-92
@@ -1,10 +1,8 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { RequestInit, Response } from 'node-fetch';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import GCS from '../client';
|
||||
import { GCS } from '../client';
|
||||
|
||||
jest.mock('node-fetch');
|
||||
|
||||
@@ -29,18 +27,15 @@ class DNSError extends ErrorWithCode {
|
||||
|
||||
const TEST_BUCKET = 'turtle-v2-test';
|
||||
|
||||
let googleApplicationCredentials: string | undefined;
|
||||
beforeAll(() => {
|
||||
googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, 'mock-credentials.json');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
|
||||
});
|
||||
function createSignedUrl(key: string, contentType: string): GCS.SignedUrl {
|
||||
return {
|
||||
url: `https://storage.googleapis.com/${TEST_BUCKET}/${key}?signature=test`,
|
||||
headers: { 'content-type': contentType },
|
||||
};
|
||||
}
|
||||
|
||||
describe('GCS client', () => {
|
||||
describe('uploadWithPresignedURL function', () => {
|
||||
describe('uploadWithSignedUrl', () => {
|
||||
it('should throw an error if upload fails', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
@@ -48,22 +43,16 @@ describe('GCS client', () => {
|
||||
status: 500,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
@@ -75,21 +64,15 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
});
|
||||
@@ -101,15 +84,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
@@ -121,8 +98,8 @@ describe('GCS client', () => {
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
@@ -135,15 +112,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
@@ -161,8 +132,8 @@ describe('GCS client', () => {
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
@@ -175,15 +146,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
@@ -198,8 +163,8 @@ describe('GCS client', () => {
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
@@ -212,15 +177,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
const lastDNSError = new DNSError('failed thrice');
|
||||
fetchMock
|
||||
@@ -237,8 +196,8 @@ describe('GCS client', () => {
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow(lastDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
@@ -251,15 +210,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
@@ -284,8 +237,8 @@ describe('GCS client', () => {
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
@@ -298,15 +251,9 @@ describe('GCS client', () => {
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
const signedUrl = createSignedUrl(key, 'image/jpeg');
|
||||
|
||||
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
|
||||
fetchMock
|
||||
@@ -317,8 +264,8 @@ describe('GCS client', () => {
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
srcGeneratorAsync: async () => Readable.from('image'),
|
||||
retryIntervalMs: 0,
|
||||
})
|
||||
).rejects.toThrow(nonDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
@@ -354,19 +301,14 @@ describe('GCS client', () => {
|
||||
} as Response;
|
||||
});
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key: 'text.txt',
|
||||
expirationTime: 30000,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
const signedUrl = createSignedUrl('text.txt', 'text/plain');
|
||||
|
||||
const bufferToUpload = randomBytes(16);
|
||||
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from(bufferToUpload),
|
||||
retryIntervalMs: 500,
|
||||
retryIntervalMs: 0,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "mock-project",
|
||||
"private_key_id": "mock-private-key-id",
|
||||
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCMk5Qbd5E8RQeP+2vObJVJVnXWXfW/OStxkAYe7r+KVXzHyLYU\nD6Ejt94OiC3X2+tqrMIZgNAUR3VF5tSiP57Il0iWHSspmNIHz7cW6/FHiQsvN2oJ\n8Fzz03vK/FfaU50QWO+BwRnixJ0xG1kSX3MxNyRZ5omGJgcwx0l6Jt878QIDAQAB\nAoGAcGZbqO6ceNU068H6/A1D/GSeSa3NHX2np+ChlEAPdJtP7yojs7yfekC801+9\nT/gurpe9hsjBF0XflemwIJ6/5KucWHrcp2cCkdBHtkK6ScHeIVl3DcOYYhxqJrAA\nYiyGNh1+pf3FMPFBcPJ04pQT4PqOif61/W3p8eJFUke5x7UCQQDoyJ427wrUCZb+\nj2GD5uVyxQa4OJxDbRFriK5LTdwLeFeOmQhoo6kaGywILn6UtlPTUJ38ROA4Jv58\nEJf/jssDAkEAmpi/JkWlOzSM9mnb9blt/fH/DRKu9z0JPvl47V5YAzBhUtUMK15g\nSbEl+AJZyaklpJcgv6ShiGY0PLdoY7oQ+wJBAMF6VX4lKqPYIKcN9ygRlm3Q8ufV\nLZQhKBRvsyYl3Zmu+V8tNL78IEXxhaR7OHxUGtINNHKDsLUbO/NUO3GOdo8CQQCV\nV0sIIK0+JUSq4ZYvqKI9d6Fnso28noSpBfuwabvh0MGjb9Viq7eeWHeSPksYSMLp\nXWiwWMwGZJy/rnk0JVEzAkADiiARp70BZgRB8ogBx+DGTravfhPP64eFMHJnUgAE\nmDKlj3v0x3OZfdUz6Us5hfy3WqFyYbcCGLf7joVVZAEm\n-----END RSA PRIVATE KEY-----",
|
||||
"client_email": "mock-client-email",
|
||||
"client_id": "mock-client-id",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-client-email",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
@@ -1,37 +1,23 @@
|
||||
import { CreateWriteStreamOptions, File, Storage } from '@google-cloud/storage';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { FetchError } from 'node-fetch';
|
||||
import stream from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import { Readable } from 'stream';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { RetryOptions, retryOnGCSUploadFailure } from './retry';
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
interface SignedUrlParams {
|
||||
key: string;
|
||||
expirationTime: number;
|
||||
contentType?: string;
|
||||
extensionHeaders?: { [key: string]: number | string | string[] };
|
||||
}
|
||||
import { retryOnGCSUploadFailure } from './retry';
|
||||
|
||||
interface UploadWithSignedUrlParams {
|
||||
signedUrl: GCS.SignedUrl;
|
||||
srcGeneratorAsync: () => Promise<Readable>;
|
||||
retryIntervalMs?: RetryOptions['retryIntervalMs'];
|
||||
retries?: RetryOptions['retries'];
|
||||
retryIntervalMs?: number;
|
||||
retries?: number;
|
||||
}
|
||||
|
||||
class GCS {
|
||||
private readonly client = new Storage();
|
||||
export namespace GCS {
|
||||
export type SignedUrl = {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
constructor(private readonly bucket: string) {
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
public static async uploadWithSignedUrl({
|
||||
export async function uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync,
|
||||
retries = 2,
|
||||
@@ -48,18 +34,15 @@ class GCS {
|
||||
body: src,
|
||||
});
|
||||
},
|
||||
{
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
}
|
||||
{ retries, retryIntervalMs }
|
||||
);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof FetchError) {
|
||||
throw new Error(`Failed to upload the file, reason: ${err.code}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
let body: string | undefined;
|
||||
try {
|
||||
@@ -69,142 +52,8 @@ class GCS {
|
||||
`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(signedUrl.url);
|
||||
return `${url.protocol}//${url.host}${url.pathname}`; // strip query string
|
||||
}
|
||||
|
||||
public formatHttpUrl(key: string): string {
|
||||
return this.client.bucket(this.bucket).file(key).publicUrl();
|
||||
}
|
||||
|
||||
public async uploadFile({
|
||||
key,
|
||||
src,
|
||||
streamOptions,
|
||||
}: {
|
||||
key: string;
|
||||
src: Readable;
|
||||
streamOptions?: CreateWriteStreamOptions;
|
||||
}): Promise<{ Location: string }> {
|
||||
const file = this.client.bucket(this.bucket).file(key);
|
||||
await new Promise<void>((res, rej) => {
|
||||
src.pipe(
|
||||
file
|
||||
.createWriteStream(streamOptions)
|
||||
.on('error', err => {
|
||||
rej(err);
|
||||
})
|
||||
.on('finish', () => {
|
||||
res();
|
||||
})
|
||||
);
|
||||
});
|
||||
return { Location: file.publicUrl() };
|
||||
}
|
||||
|
||||
public async deleteFile(key: string): Promise<void> {
|
||||
try {
|
||||
await this.client.bucket(this.bucket).file(key).delete();
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode === 404) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime,
|
||||
contentType,
|
||||
extensionHeaders = {},
|
||||
}: SignedUrlParams): Promise<GCS.SignedUrl> {
|
||||
const config = {
|
||||
version: 'v4' as const,
|
||||
action: 'write' as const,
|
||||
expires: Date.now() + expirationTime,
|
||||
contentType,
|
||||
extensionHeaders,
|
||||
};
|
||||
|
||||
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
|
||||
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
...(contentType ? { 'content-type': contentType } : {}),
|
||||
...extensionHeaders,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async createSignedDownloadUrl({
|
||||
key,
|
||||
expirationTime,
|
||||
}: {
|
||||
key: string;
|
||||
expirationTime: number;
|
||||
}): Promise<string> {
|
||||
const options = {
|
||||
version: 'v4' as const,
|
||||
action: 'read' as const,
|
||||
expires: Date.now() + expirationTime,
|
||||
};
|
||||
|
||||
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
public async checkIfFileExists(key: string, fileHash?: string): Promise<boolean> {
|
||||
let metadata;
|
||||
try {
|
||||
[metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
|
||||
} catch (error: any) {
|
||||
if (error.code === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fileHash ? metadata.etag === fileHash : true;
|
||||
}
|
||||
|
||||
public async listDirectory(prefix: string): Promise<string[]> {
|
||||
const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
|
||||
return files.map(x => this.formatHttpUrl(x.name));
|
||||
}
|
||||
|
||||
public async moveFile(src: string, dest: string): Promise<void> {
|
||||
await this.client.bucket(this.bucket).file(src).move(dest);
|
||||
}
|
||||
|
||||
public async deleteFiles(keys: string[]): Promise<void> {
|
||||
await Promise.all(keys.map(key => this.deleteFile(key)));
|
||||
}
|
||||
|
||||
public async downloadFile(key: string, destinationPath: string): Promise<void> {
|
||||
const stream = this.client.bucket(this.bucket).file(key).createReadStream();
|
||||
await pipeline(stream, fs.createWriteStream(destinationPath));
|
||||
}
|
||||
|
||||
public getFile(key: string): File {
|
||||
return this.client.bucket(this.bucket).file(key);
|
||||
return `${url.protocol}//${url.host}${url.pathname}`;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GCS {
|
||||
export interface SignedUrl {
|
||||
url: string;
|
||||
headers: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default GCS;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
export interface RetryOptions {
|
||||
type RetryOptions = {
|
||||
retries: number;
|
||||
retryIntervalMs: number;
|
||||
shouldRetryOnError: (error: any) => boolean;
|
||||
shouldRetryOnError: (error: unknown) => boolean;
|
||||
shouldRetryOnResponse: (response: Response) => boolean;
|
||||
}
|
||||
};
|
||||
|
||||
// based on https://github.com/googleapis/nodejs-storage/blob/8ab50804fc7bae3bbd159bbb4adf65c02215b11b/src/storage.ts#L284-L320
|
||||
export async function retryOnGCSUploadFailure(
|
||||
@@ -18,13 +18,14 @@ export async function retryOnGCSUploadFailure(
|
||||
return await retry(fn, {
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
shouldRetryOnError: e => {
|
||||
shouldRetryOnError: err => {
|
||||
return (
|
||||
e.code === 'ENOTFOUND' ||
|
||||
e.code === 'EAI_AGAIN' ||
|
||||
e.code === 'ECONNRESET' ||
|
||||
e.code === 'ETIMEDOUT' ||
|
||||
e.code === 'EPIPE'
|
||||
isErrorWithCode(err) &&
|
||||
(err.code === 'ENOTFOUND' ||
|
||||
err.code === 'EAI_AGAIN' ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'ETIMEDOUT' ||
|
||||
err.code === 'EPIPE')
|
||||
);
|
||||
},
|
||||
shouldRetryOnResponse: resp => {
|
||||
@@ -33,8 +34,12 @@ export async function retryOnGCSUploadFailure(
|
||||
});
|
||||
}
|
||||
|
||||
function isErrorWithCode(error: unknown): error is { code: string } {
|
||||
return typeof error === 'object' && error !== null && 'code' in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper used to execute an inner function and possibly retry it if it throws and error
|
||||
* Wrapper used to execute an inner function and possibly retry it if it throws an error
|
||||
* @param fn Function to be executed and retried in case of error
|
||||
* @param retries How many times at most should the function be retried
|
||||
* @param retryIntervalMs Time interval between the retries
|
||||
@@ -55,7 +60,7 @@ async function retry(
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (attemptCount === retries || !shouldRetryOnError(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Builders from './builders';
|
||||
import GCSLoggerStream from './gcs/LoggerStream';
|
||||
import GCS from './gcs/client';
|
||||
import { GCS } from './gcs/client';
|
||||
|
||||
export { Builders, GCS, GCSLoggerStream };
|
||||
|
||||
|
||||
+82
-3
@@ -12,7 +12,23 @@ jest.mock('../../utils/deviceRunSessionArtifacts', () => ({
|
||||
uploadDeviceRunSessionArtifactAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
const SIMULATOR_UDID = '01234567-89AB-CDEF-0123-456789ABCDEF';
|
||||
const SECOND_SIMULATOR_UDID = 'FEDCBA98-7654-3210-FEDC-BA9876543210';
|
||||
|
||||
describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.mocked(uploadDeviceRunSessionArtifactAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(async (_ctx, { stream }) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.once('end', resolve);
|
||||
stream.once('error', reject);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uploads simulator device metadata', async () => {
|
||||
const recordingDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'ios-recording-test-'));
|
||||
const recordingPath = path.join(recordingDirectory, 'recording.mp4');
|
||||
@@ -22,6 +38,8 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
|
||||
JSON.stringify({
|
||||
recording: path.basename(recordingPath),
|
||||
firstFrameWallClock: { iso8601: '2026-07-10T10:00:00.000Z' },
|
||||
width: 1179,
|
||||
height: 2556,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -36,7 +54,7 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
|
||||
callInputs: {
|
||||
recordings_json: [
|
||||
{
|
||||
udid: 'simulator-udid',
|
||||
udid: SIMULATOR_UDID,
|
||||
deviceName: 'iPhone 16',
|
||||
runtimeDisplayName: 'iOS 18.6',
|
||||
directory: recordingDirectory,
|
||||
@@ -50,13 +68,16 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
|
||||
expect(uploadDeviceRunSessionArtifactAsync).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
name: 'iPhone 16 screen recording',
|
||||
name: 'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
|
||||
metadata: {
|
||||
__eas_type: 'screen-recording',
|
||||
__eas_screen_recording: '1',
|
||||
udid: 'simulator-udid',
|
||||
udid: SIMULATOR_UDID,
|
||||
deviceName: 'iPhone 16',
|
||||
runtimeDisplayName: 'iOS 18.6',
|
||||
firstFrameAt: '2026-07-10T10:00:00.000Z',
|
||||
width: 1179,
|
||||
height: 2556,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -64,4 +85,62 @@ describe(createUploadDeviceRunSessionScreenRecordingsBuildFunction, () => {
|
||||
await fs.rm(recordingDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('uses unique names for recordings from rebooted and same-model simulators', async () => {
|
||||
const recordingDirectories = await Promise.all(
|
||||
['2026-07-10T10:00:00.000Z', '2026-07-10T10:05:00.000Z', '2026-07-10T10:00:00.000Z'].map(
|
||||
async firstFrameAt => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ios-recording-test-'));
|
||||
await fs.writeFile(path.join(directory, 'recording.mp4'), 'recording');
|
||||
await fs.writeFile(
|
||||
path.join(directory, 'session.json'),
|
||||
JSON.stringify({
|
||||
recording: 'recording.mp4',
|
||||
firstFrameWallClock: { iso8601: firstFrameAt },
|
||||
width: 1179,
|
||||
height: 2556,
|
||||
})
|
||||
);
|
||||
return directory;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const globalContext = createGlobalContextMock({
|
||||
runtimePlatform: BuildRuntimePlatform.DARWIN,
|
||||
});
|
||||
const buildStep = createUploadDeviceRunSessionScreenRecordingsBuildFunction(
|
||||
{} as CustomBuildContext
|
||||
).createBuildStepFromFunctionCall(globalContext, {
|
||||
env: { DEVICE_RUN_SESSION_ID: 'device-run-session-id' },
|
||||
callInputs: {
|
||||
recordings_json: recordingDirectories.map((directory, index) => ({
|
||||
udid: index === 2 ? SECOND_SIMULATOR_UDID : SIMULATOR_UDID,
|
||||
deviceName: 'iPhone 16',
|
||||
runtimeDisplayName: 'iOS 18.6',
|
||||
directory,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
await buildStep.executeAsync();
|
||||
|
||||
const names = jest
|
||||
.mocked(uploadDeviceRunSessionArtifactAsync)
|
||||
.mock.calls.map(([, options]) => options.name);
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining([
|
||||
'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
|
||||
'iPhone 16 screen recording (01234567-…, started at Jul 10, 2026, 10:05:00.000 UTC)',
|
||||
'iPhone 16 screen recording (FEDCBA98-…, started at Jul 10, 2026, 10:00:00.000 UTC)',
|
||||
])
|
||||
);
|
||||
expect(new Set(names).size).toBe(3);
|
||||
} finally {
|
||||
await Promise.all(
|
||||
recordingDirectories.map(directory => fs.rm(directory, { recursive: true, force: true }))
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -29,19 +29,18 @@ export function createStartServeSimRemoteSessionBuildFunction(
|
||||
|
||||
await selectXcodeDeveloperDirectoryAsync({ env, logger });
|
||||
|
||||
const { previewUrl, streamUrl } = await startServeSimWithTunnelAsync(ctx, {
|
||||
const { previewUrl } = await startServeSimWithTunnelAsync(ctx, {
|
||||
baseDomain: ngrokTunnelDomain,
|
||||
env,
|
||||
logger,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
logger.info(`Preview URL: ${previewUrl}`);
|
||||
logger.info(`Stream URL: ${streamUrl}`);
|
||||
|
||||
await uploadRemoteSessionConfigAsync({
|
||||
ctx,
|
||||
deviceRunSessionId,
|
||||
remoteConfig: { previewUrl, streamUrl },
|
||||
remoteConfig: { previewUrl },
|
||||
logger,
|
||||
});
|
||||
|
||||
|
||||
@@ -30,9 +30,24 @@ const RecordingManifestSchema = z.object({
|
||||
firstFrameWallClock: z.object({
|
||||
iso8601: z.string(),
|
||||
}),
|
||||
width: z.number().int().positive(),
|
||||
height: z.number().int().positive(),
|
||||
recording: z.string(),
|
||||
});
|
||||
|
||||
const recordingStartTimeFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3,
|
||||
hourCycle: 'h23',
|
||||
timeZone: 'UTC',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
|
||||
export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
|
||||
ctx: CustomBuildContext
|
||||
): BuildFunction {
|
||||
@@ -68,11 +83,15 @@ export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
|
||||
await Promise.all(
|
||||
recordings.map(recording =>
|
||||
limit(async () => {
|
||||
const displayName = `${recording.deviceName} screen recording`;
|
||||
try {
|
||||
const metadata = RecordingManifestSchema.parse(
|
||||
JSON.parse(await readFile(path.join(recording.directory, 'session.json'), 'utf-8'))
|
||||
);
|
||||
const startedAt = recordingStartTimeFormatter.format(
|
||||
new Date(metadata.firstFrameWallClock.iso8601)
|
||||
);
|
||||
const shortUdid = `${recording.udid.slice(0, 8)}-…`;
|
||||
const displayName = `${recording.deviceName} screen recording (${shortUdid}, started at ${startedAt})`;
|
||||
const recordingPath = path.join(recording.directory, metadata.recording);
|
||||
const { size } = await stat(recordingPath);
|
||||
const recordingId = path.basename(recording.directory);
|
||||
@@ -86,11 +105,14 @@ export function createUploadDeviceRunSessionScreenRecordingsBuildFunction(
|
||||
filename: `${recordingId}.mp4`,
|
||||
kind: 'screen-recording',
|
||||
metadata: {
|
||||
__eas_type: 'screen-recording',
|
||||
__eas_screen_recording: '1',
|
||||
udid: recording.udid,
|
||||
deviceName: recording.deviceName,
|
||||
runtimeDisplayName: recording.runtimeDisplayName,
|
||||
firstFrameAt: metadata.firstFrameWallClock.iso8601,
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
},
|
||||
size,
|
||||
stream: createReadStream(recordingPath),
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Sentry } from '../../sentry';
|
||||
import { IosSimulatorUtils, type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
|
||||
|
||||
const IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS = 2_000;
|
||||
const RECORD_SIM_FINISH_TIMEOUT_MS = 30_000;
|
||||
const RECORD_SIM_FINISH_TIMEOUT_MS = 70_000;
|
||||
const RECORD_SIM_FORCE_STOP_TIMEOUT_MS = 5_000;
|
||||
const RECORD_SIM_MAX_ATTEMPTS_PER_BOOT = 3;
|
||||
const RECORD_SIM_COMMAND = 'record-sim';
|
||||
@@ -114,7 +114,16 @@ export namespace IosSimulatorRecordingUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Forcing iOS Simulator recording process for ${recording.deviceName} to stop.`);
|
||||
const recordSimOutput = recording.getOutput().trim();
|
||||
const finishTimeoutSeconds = Math.round(RECORD_SIM_FINISH_TIMEOUT_MS / 1_000);
|
||||
logger.warn(
|
||||
{ recordSimOutput },
|
||||
`Screen recording for ${recording.deviceName} did not finish within ${finishTimeoutSeconds} seconds and will be stopped.${
|
||||
recordSimOutput
|
||||
? `\nRecent recorder messages:\n${recordSimOutput}`
|
||||
: '\nNo recorder messages were captured.'
|
||||
}`
|
||||
);
|
||||
recording.recordingProcess.kill('SIGKILL');
|
||||
const killed = await Promise.race([
|
||||
recording.completionPromise.then(() => true),
|
||||
|
||||
@@ -343,7 +343,7 @@ export async function startServeSimWithTunnelAsync(
|
||||
logger: bunyan;
|
||||
timeoutMs: number;
|
||||
}
|
||||
): Promise<{ previewUrl: string; streamUrl: string }> {
|
||||
): Promise<{ previewUrl: string }> {
|
||||
logger.info('Launching serve-sim with tunnel.');
|
||||
const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
|
||||
const serveSim = spawnDetached({
|
||||
@@ -366,33 +366,30 @@ export async function startServeSimWithTunnelAsync(
|
||||
env,
|
||||
});
|
||||
|
||||
logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
|
||||
logger.info('Waiting for serve-sim to report tunnel URL.');
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const output = serveSim.getOutput();
|
||||
const previewUrl = matchLabeledUrl({ output, label: 'Tunnel', baseDomain });
|
||||
const streamUrl = matchLabeledUrl({ output, label: 'Stream', baseDomain });
|
||||
if (previewUrl && streamUrl) {
|
||||
return { previewUrl, streamUrl };
|
||||
const previewUrl = matchTunnelUrl({ output, baseDomain });
|
||||
if (previewUrl) {
|
||||
return { previewUrl };
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new SystemError(
|
||||
`Timed out waiting for serve-sim to report Tunnel and Stream URLs. Last output:\n${serveSim.getOutput() || '<empty>'}`
|
||||
`Timed out waiting for serve-sim to report Tunnel URL. Last output:\n${serveSim.getOutput() || '<empty>'}`
|
||||
);
|
||||
}
|
||||
|
||||
function matchLabeledUrl({
|
||||
function matchTunnelUrl({
|
||||
output,
|
||||
label,
|
||||
baseDomain,
|
||||
}: {
|
||||
output: string;
|
||||
label: string;
|
||||
baseDomain: string;
|
||||
}): string | null {
|
||||
const labelPattern = new RegExp(
|
||||
`${label}:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`
|
||||
`Tunnel:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`
|
||||
);
|
||||
const match = labelPattern.exec(output);
|
||||
return match ? match[1] : null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "21.0.1"
|
||||
"@expo/steps": "21.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HOOK_ANCHORS, parseHookKey } from '../hooks';
|
||||
|
||||
describe('HOOK_ANCHORS', () => {
|
||||
it('contains the v1 anchors', () => {
|
||||
expect(Object.keys(HOOK_ANCHORS).sort()).toEqual([
|
||||
expect([...HOOK_ANCHORS].sort()).toEqual([
|
||||
'checkout',
|
||||
'install_node_modules',
|
||||
'maestro_cloud',
|
||||
@@ -10,12 +10,6 @@ describe('HOOK_ANCHORS', () => {
|
||||
'submit',
|
||||
]);
|
||||
});
|
||||
|
||||
it('every entry has a non-empty description', () => {
|
||||
for (const entry of Object.values(HOOK_ANCHORS)) {
|
||||
expect(entry.description.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe(parseHookKey, () => {
|
||||
|
||||
@@ -12,31 +12,24 @@
|
||||
* inside a function-group expansion — is an occurrence of the anchor). Several
|
||||
* steps and several functions may declare the same anchor.
|
||||
*/
|
||||
export const HOOK_ANCHORS = {
|
||||
install_node_modules: {
|
||||
description: 'The step that installs node modules.',
|
||||
},
|
||||
submit: {
|
||||
description: 'The step that submits the application binary to the store.',
|
||||
},
|
||||
maestro_tests: {
|
||||
description: 'The step that runs Maestro tests.',
|
||||
},
|
||||
maestro_cloud: {
|
||||
description: 'The steps that upload to Maestro Cloud and gather its results.',
|
||||
},
|
||||
checkout: {
|
||||
description: 'The step that checks out the project sources.',
|
||||
},
|
||||
} as const satisfies Record<string, { description: string }>;
|
||||
export const HOOK_ANCHORS = [
|
||||
'install_node_modules',
|
||||
'submit',
|
||||
'maestro_tests',
|
||||
'maestro_cloud',
|
||||
'checkout',
|
||||
] as const;
|
||||
|
||||
export type HookAnchorId = keyof typeof HOOK_ANCHORS;
|
||||
export type HookAnchorId = (typeof HOOK_ANCHORS)[number];
|
||||
export type HookKey = `before_${HookAnchorId}` | `after_${HookAnchorId}`;
|
||||
|
||||
export const HOOK_KEYS: readonly HookKey[] = HOOK_ANCHORS.flatMap((anchor): HookKey[] => [
|
||||
`before_${anchor}`,
|
||||
`after_${anchor}`,
|
||||
]);
|
||||
|
||||
export function isHookAnchorId(value: string): value is HookAnchorId {
|
||||
// Own-property check: `in` would also match Object prototype names
|
||||
// ("toString", "__proto__", …), breaking the unknown-anchors-are-inert rule.
|
||||
return Object.hasOwn(HOOK_ANCHORS, value);
|
||||
return HOOK_ANCHORS.some(anchor => anchor === value);
|
||||
}
|
||||
|
||||
export function parseHookKey(
|
||||
|
||||
+114
-114
@@ -205,7 +205,7 @@ DESCRIPTION
|
||||
view the audit logs for an account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/account/audit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/audit.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/account/audit.ts)_
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -226,7 +226,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -243,7 +243,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -265,7 +265,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/v21.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/v21.0.2/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -282,7 +282,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -296,7 +296,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -348,7 +348,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -369,7 +369,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -389,7 +389,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -409,7 +409,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -432,7 +432,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas browse [PAGE]`
|
||||
|
||||
@@ -455,7 +455,7 @@ DESCRIPTION
|
||||
Transition from the terminal to the web browser to view and interact with your project on https://expo.dev
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/browse.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/browse.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/browse.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -496,7 +496,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -516,7 +516,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -534,7 +534,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -554,7 +554,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -579,7 +579,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -606,7 +606,7 @@ DESCRIPTION
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -644,7 +644,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -684,7 +684,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -714,7 +714,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -742,7 +742,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -794,7 +794,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -812,7 +812,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -830,7 +830,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -847,7 +847,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/v21.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/v21.0.2/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -868,7 +868,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -889,7 +889,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -911,7 +911,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/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:insights`
|
||||
|
||||
@@ -935,7 +935,7 @@ DESCRIPTION
|
||||
display adoption, crash, and unique-user insights for a channel + runtime version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -955,7 +955,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -977,7 +977,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/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -999,7 +999,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/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -1034,7 +1034,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/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -1057,7 +1057,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -1078,7 +1078,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/v21.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/v21.0.2/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -1095,7 +1095,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1113,7 +1113,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/v21.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/v21.0.2/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1145,7 +1145,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1170,7 +1170,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1191,7 +1191,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1212,7 +1212,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1249,7 +1249,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/v21.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/v21.0.2/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1269,7 +1269,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/v21.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/v21.0.2/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1290,7 +1290,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/v21.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/v21.0.2/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1311,7 +1311,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1325,7 +1325,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/v21.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/v21.0.2/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1339,7 +1339,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1371,7 +1371,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1397,7 +1397,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1419,7 +1419,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1447,7 +1447,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1475,7 +1475,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1498,7 +1498,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1520,7 +1520,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1554,7 +1554,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/v21.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/v21.0.2/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1598,7 +1598,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/v21.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/v21.0.2/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1629,7 +1629,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/v21.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/v21.0.2/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1692,7 +1692,7 @@ DESCRIPTION
|
||||
connect a project to an App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
|
||||
## `eas integrations:asc:disconnect`
|
||||
|
||||
@@ -1711,7 +1711,7 @@ DESCRIPTION
|
||||
disconnect the current project from its App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
|
||||
## `eas integrations:asc:status`
|
||||
|
||||
@@ -1729,7 +1729,7 @@ DESCRIPTION
|
||||
show the App Store Connect app link status for the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
|
||||
## `eas integrations:convex:connect`
|
||||
|
||||
@@ -1751,7 +1751,7 @@ DESCRIPTION
|
||||
connect Convex to your Expo project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
|
||||
## `eas integrations:convex:dashboard`
|
||||
|
||||
@@ -1765,7 +1765,7 @@ DESCRIPTION
|
||||
open the Convex dashboard for the linked Convex project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
|
||||
## `eas integrations:convex:project`
|
||||
|
||||
@@ -1779,7 +1779,7 @@ DESCRIPTION
|
||||
display the Convex project linked to the current Expo app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
|
||||
## `eas integrations:convex:project:delete`
|
||||
|
||||
@@ -1797,7 +1797,7 @@ DESCRIPTION
|
||||
remove the Convex project link for the current Expo app from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team`
|
||||
|
||||
@@ -1811,7 +1811,7 @@ DESCRIPTION
|
||||
display Convex teams linked to the current Expo app's owner account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
|
||||
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
|
||||
|
||||
@@ -1832,7 +1832,7 @@ DESCRIPTION
|
||||
remove a Convex team link from the current Expo app owner account's EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
|
||||
|
||||
@@ -1852,7 +1852,7 @@ DESCRIPTION
|
||||
send a Convex team invitation to your verified email address
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
|
||||
## `eas integrations:posthog:connect`
|
||||
|
||||
@@ -1879,7 +1879,7 @@ DESCRIPTION
|
||||
connect PostHog to your Expo project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/connect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/connect.ts)_
|
||||
|
||||
## `eas integrations:posthog:dashboard`
|
||||
|
||||
@@ -1899,7 +1899,7 @@ DESCRIPTION
|
||||
open the PostHog dashboard for the linked PostHog project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/dashboard.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/dashboard.ts)_
|
||||
|
||||
## `eas integrations:posthog:disconnect`
|
||||
|
||||
@@ -1918,7 +1918,7 @@ DESCRIPTION
|
||||
remove the PostHog project link for the current Expo app from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/posthog/disconnect.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/integrations/posthog/disconnect.ts)_
|
||||
|
||||
## `eas login`
|
||||
|
||||
@@ -1970,7 +1970,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1988,7 +1988,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/v21.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/v21.0.2/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -2006,7 +2006,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/v21.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/v21.0.2/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -2065,7 +2065,7 @@ DESCRIPTION
|
||||
arguments, a list of the available event names and associated event counts is returned.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/events.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/events.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/events.ts)_
|
||||
|
||||
## `eas observe:metrics [METRIC]`
|
||||
|
||||
@@ -2101,7 +2101,7 @@ DESCRIPTION
|
||||
display individual performance metric samples ordered by value
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/metrics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/metrics.ts)_
|
||||
|
||||
## `eas observe:metrics-summary`
|
||||
|
||||
@@ -2133,7 +2133,7 @@ DESCRIPTION
|
||||
display aggregated performance metric statistics grouped by app version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/metrics-summary.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/metrics-summary.ts)_
|
||||
|
||||
## `eas observe:routes`
|
||||
|
||||
@@ -2170,7 +2170,7 @@ DESCRIPTION
|
||||
display app navigation route metrics (Cold TTR, Warm TTR, TTI) grouped by route name
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/routes.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/routes.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/routes.ts)_
|
||||
|
||||
## `eas observe:session SESSIONID`
|
||||
|
||||
@@ -2192,7 +2192,7 @@ DESCRIPTION
|
||||
display the timeline of metric and log events for a specific session
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/session.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/session.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/session.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/session.ts)_
|
||||
|
||||
## `eas observe:versions`
|
||||
|
||||
@@ -2217,7 +2217,7 @@ DESCRIPTION
|
||||
display app versions with build and update details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/observe/versions.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/observe/versions.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/observe/versions.ts)_
|
||||
|
||||
## `eas project:delete [NAME]`
|
||||
|
||||
@@ -2241,7 +2241,7 @@ DESCRIPTION
|
||||
delete a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/project/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/project/delete.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -2255,7 +2255,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -2278,7 +2278,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -2302,7 +2302,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -2335,7 +2335,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -2378,7 +2378,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -2399,7 +2399,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -2420,7 +2420,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/v21.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/v21.0.2/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -2445,7 +2445,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/v21.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/v21.0.2/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:embedded:delete ID`
|
||||
|
||||
@@ -2466,7 +2466,7 @@ DESCRIPTION
|
||||
delete an embedded update registered with EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/delete.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/delete.ts)_
|
||||
|
||||
## `eas update:embedded:list`
|
||||
|
||||
@@ -2492,7 +2492,7 @@ DESCRIPTION
|
||||
list embedded updates registered with EAS Update for this project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/list.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/list.ts)_
|
||||
|
||||
## `eas update:embedded:upload`
|
||||
|
||||
@@ -2522,7 +2522,7 @@ EXAMPLES
|
||||
$ eas update:embedded:upload --platform android --bundle android/app/src/main/assets/index.android.bundle --manifest android/app/src/main/assets/app.manifest --channel production --build-id <BUILD-ID>
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/upload.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/upload.ts)_
|
||||
|
||||
## `eas update:embedded:view ID`
|
||||
|
||||
@@ -2542,7 +2542,7 @@ DESCRIPTION
|
||||
view details of an embedded update registered with EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/embedded/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/embedded/view.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/embedded/view.ts)_
|
||||
|
||||
## `eas update:insights GROUPID`
|
||||
|
||||
@@ -2569,7 +2569,7 @@ DESCRIPTION
|
||||
display launch, crash, unique-user, and size insights for an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.1/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v21.0.2/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -2596,7 +2596,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2634,7 +2634,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/v21.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/v21.0.2/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2662,7 +2662,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/v21.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/v21.0.2/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2692,7 +2692,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/v21.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/v21.0.2/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback [GROUPID]`
|
||||
|
||||
@@ -2724,7 +2724,7 @@ DESCRIPTION
|
||||
roll back to an embedded update or an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2748,7 +2748,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2769,7 +2769,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/v21.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/v21.0.2/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2791,7 +2791,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2811,7 +2811,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2830,7 +2830,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2853,7 +2853,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2870,7 +2870,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2994,7 +2994,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -3016,7 +3016,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -3040,7 +3040,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -3080,7 +3080,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -3102,7 +3102,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -3130,7 +3130,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -3150,7 +3150,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -3172,5 +3172,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/v21.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/v21.0.2/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+507
-10
@@ -12174,6 +12174,87 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workflowCachesPaginated",
|
||||
"description": "Caches associated with this app, ordered by last access time (most recent first).",
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppWorkflowCachesConnection",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workflowCachingConfig",
|
||||
"description": "Per-type caching configuration for this app.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCachingConfig",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workflowDeviceTestCaseHistory",
|
||||
"description": null,
|
||||
@@ -13802,6 +13883,55 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "setWorkflowCachingConfig",
|
||||
"description": "Set per-type caching configuration for the app.",
|
||||
"args": [
|
||||
{
|
||||
"name": "appId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "WorkflowCachingConfigInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "App",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -23454,6 +23584,100 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppWorkflowCacheEdge",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCache",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppWorkflowCachesConnection",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppWorkflowCacheEdge",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppWorkflowFilterInput",
|
||||
@@ -64215,6 +64439,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workflowCache",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCacheMutation",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workflowDeviceTestCaseResult",
|
||||
"description": null,
|
||||
@@ -67765,16 +68005,12 @@
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use previewUrl instead."
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -75247,6 +75483,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "searchTerm",
|
||||
"description": "Case-insensitive substring match on update group message and branch name.\nEmbedded updates are matched on channel and runtime version.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "types",
|
||||
"description": null,
|
||||
@@ -86284,6 +86532,255 @@
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCache",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "createdAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "creatingActor",
|
||||
"description": "Actor who created this cache. Null if the creating user no longer exists.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "INTERFACE",
|
||||
"name": "Actor",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "gitBranchName",
|
||||
"description": "Git branch name the cache was created from. Null for user-scoped caches.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "hydratingBuild",
|
||||
"description": "Build that created this cache. Null if the cache was created by a job run\nor the build no longer exists.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Build",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "hydratingJobRun",
|
||||
"description": "Job run that created this cache. Null if the cache was created by a build\nor the job run no longer exists.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "JobRun",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"description": "Cache key used to identify the cache entry.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastAccessedAt",
|
||||
"description": "Time the cache was last used to restore, or the creation time if it has\nnever been restored.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sizeBytes",
|
||||
"description": "Size of the cache archive in bytes. Null if the cache has not been hydrated yet.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Float",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCacheMutation",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "deleteWorkflowCache",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "cacheId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCache",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowCachingConfig",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "ccacheEnabled",
|
||||
"description": "Whether the ccache compiler cache is enabled. Null if not explicitly\nconfigured, in which case the account-level default applies.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "gradleCacheEnabled",
|
||||
"description": "Whether the Gradle build cache is enabled. Null if not explicitly\nconfigured, in which case it is disabled.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "WorkflowCachingConfigInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "ccacheEnabled",
|
||||
"description": "Pass true or false to explicitly enable or disable, null to reset to the account-level\ndefault, or omit the field to leave it unchanged.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "gradleCacheEnabled",
|
||||
"description": "Pass true or false to explicitly enable or disable, null to reset to the default\n(disabled), or omit the field to leave it unchanged.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "WorkflowDeviceTestCaseErrorPattern",
|
||||
@@ -93115,4 +93612,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -48,8 +48,8 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/eas-build-job": "21.0.1",
|
||||
"@expo/eas-json": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"@expo/eas-json": "21.0.2",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "21.0.0",
|
||||
@@ -57,14 +57,14 @@
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/pkcs12": "0.1.3",
|
||||
"@expo/plist": "0.2.0",
|
||||
"@expo/plist": "0.3.5",
|
||||
"@expo/plugin-help": "5.1.23",
|
||||
"@expo/plugin-warn-if-update-available": "2.5.1",
|
||||
"@expo/prebuild-config": "8.0.17",
|
||||
"@expo/prebuild-config": "9.0.12",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "21.0.1",
|
||||
"@expo/steps": "21.0.2",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
@@ -134,17 +134,16 @@
|
||||
"zod": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@graphql-codegen/cli": "6.3.1",
|
||||
"@graphql-codegen/introspection": "5.0.2",
|
||||
"@graphql-codegen/typescript": "5.0.10",
|
||||
"@graphql-codegen/typescript-operations": "5.1.0",
|
||||
"@tsconfig/node20": "20.1.8",
|
||||
"@types/cli-progress": "3.11.5",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/debug": "4.1.13",
|
||||
"@types/diff": "6.0.0",
|
||||
"@types/envinfo": "7.8.3",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/invariant": "^2.2.37",
|
||||
@@ -160,9 +159,8 @@
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "1.11.0",
|
||||
"axios": "1.18.1",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.20.0",
|
||||
"jest": "29.7.0",
|
||||
"memfs": "3.4.13",
|
||||
"mockdate": "3.0.5",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { runBuildAndSubmitAsync } from '../../../build/runBuildAndSubmit';
|
||||
import BuildInspect from '../inspect';
|
||||
|
||||
jest.mock('fs-extra', () => ({
|
||||
pathExists: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
mkdirp: jest.fn(),
|
||||
copy: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../build/runBuildAndSubmit', () => ({
|
||||
runBuildAndSubmitAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn(() => ({
|
||||
start: jest.fn(() => ({
|
||||
succeed: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
jest.mock('../../../utils/paths', () => ({
|
||||
...jest.requireActual('../../../utils/paths'),
|
||||
getTmpDirectory: jest.fn(() => '/tmp/eas-cli'),
|
||||
}));
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'tmp-id'),
|
||||
}));
|
||||
|
||||
describe(BuildInspect, () => {
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const tmpWorkingdir = '/tmp/eas-cli/tmp-id';
|
||||
const tmpBuildDir = path.join(tmpWorkingdir, 'build');
|
||||
const outputDirectory = path.join(process.cwd(), 'inspect-output');
|
||||
const mockPathExists = fs.pathExists as jest.Mock;
|
||||
const mockMkdirp = fs.mkdirp as jest.Mock;
|
||||
const mockCopy = fs.copy as jest.Mock;
|
||||
const mockRemove = fs.remove as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockPathExists.mockResolvedValue(false);
|
||||
mockMkdirp.mockResolvedValue(undefined);
|
||||
mockCopy.mockResolvedValue(undefined);
|
||||
mockRemove.mockResolvedValue(undefined);
|
||||
jest.mocked(runBuildAndSubmitAsync).mockResolvedValue({ buildIds: [] });
|
||||
});
|
||||
|
||||
function createCommand(): BuildInspect {
|
||||
const command = new BuildInspect(
|
||||
['--platform', 'ios', '--stage', 'pre-build', '--output', 'inspect-output'],
|
||||
mockConfig
|
||||
);
|
||||
jest.spyOn(command as any, 'getContextAsync').mockResolvedValue({
|
||||
loggedIn: {
|
||||
actor: {},
|
||||
graphqlClient: {},
|
||||
},
|
||||
getDynamicPrivateProjectConfigAsync: jest.fn(),
|
||||
projectDir: '/project',
|
||||
analytics: {},
|
||||
vcsClient: {},
|
||||
} as any);
|
||||
return command;
|
||||
}
|
||||
|
||||
it('removes the temporary working directory after copying output when the build fails', async () => {
|
||||
jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed'));
|
||||
mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
|
||||
await createCommand().runAsync();
|
||||
|
||||
expect(fs.copy).toHaveBeenCalledWith(tmpBuildDir, outputDirectory);
|
||||
expect(fs.remove).toHaveBeenCalledWith(tmpBuildDir);
|
||||
expect(fs.remove).toHaveBeenCalledWith(tmpWorkingdir);
|
||||
});
|
||||
|
||||
it('keeps the temporary working directory when copying output fails', async () => {
|
||||
jest.mocked(runBuildAndSubmitAsync).mockRejectedValue(new Error('build failed'));
|
||||
mockPathExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
mockCopy.mockRejectedValue(new Error('copy failed'));
|
||||
|
||||
await expect(createCommand().runAsync()).rejects.toThrow('copy failed');
|
||||
|
||||
expect(fs.remove).not.toHaveBeenCalledWith(tmpBuildDir);
|
||||
expect(fs.remove).not.toHaveBeenCalledWith(tmpWorkingdir);
|
||||
});
|
||||
});
|
||||
@@ -135,6 +135,7 @@ export default class BuildInspect extends EasCommand {
|
||||
}
|
||||
} finally {
|
||||
await this.copyToOutputDirAsync(path.join(tmpWorkingdir, 'build'), outputDirectory);
|
||||
await this.cleanUpTmpWorkingdirAsync(tmpWorkingdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,4 +164,13 @@ export default class BuildInspect extends EasCommand {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanUpTmpWorkingdirAsync(tmpWorkingdir: string): Promise<void> {
|
||||
try {
|
||||
await fs.remove(tmpWorkingdir);
|
||||
} catch (err) {
|
||||
Log.warn(`Failed to remove temporary directory ${tmpWorkingdir}`);
|
||||
Log.debug(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,8 +352,8 @@ describe(EnvPull, () => {
|
||||
});
|
||||
|
||||
describe('existing .env.local file handling', () => {
|
||||
it('preserves existing secret values when they exist', async () => {
|
||||
const existingEnvContent = 'SECRET_KEY=existing-secret-value\nOTHER_VAR=other-value';
|
||||
it.each(['existing-secret-value', ''])('preserves an existing secret value %j', async value => {
|
||||
const existingEnvContent = `SECRET_KEY=${value}\nOTHER_VAR=other-value`;
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(existingEnvContent));
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
@@ -376,7 +376,7 @@ describe(EnvPull, () => {
|
||||
const fileContent = envFileCall![1] as string;
|
||||
|
||||
// Should use existing secret value instead of placeholder
|
||||
expect(fileContent).toContain('SECRET_KEY=existing-secret-value');
|
||||
expect(fileContent).toContain(`SECRET_KEY=${value}`);
|
||||
expect(fileContent).not.toContain('# SECRET_KEY=***** (secret)');
|
||||
|
||||
// Should log that it reused the local value
|
||||
|
||||
+33
-9
@@ -1,5 +1,4 @@
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
@@ -12,6 +11,10 @@ import {
|
||||
} from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import {
|
||||
formatEnvironmentVariableDiffAsync,
|
||||
getEnvironmentVariableNamesFromEnvFile,
|
||||
} from '../../utils/environmentVariableDiff';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
|
||||
export default class EnvPull extends EasCommand {
|
||||
@@ -76,7 +79,8 @@ export default class EnvPull extends EasCommand {
|
||||
}
|
||||
);
|
||||
|
||||
if (!nonInteractive && (await fs.exists(targetPath))) {
|
||||
const targetExists = await fs.exists(targetPath);
|
||||
if (!nonInteractive && targetExists) {
|
||||
const result = await confirmAsync({
|
||||
message: `File ${targetPath} already exists. Do you want to overwrite it?`,
|
||||
});
|
||||
@@ -87,9 +91,12 @@ export default class EnvPull extends EasCommand {
|
||||
}
|
||||
|
||||
let currentEnvLocal: Record<string, string> = {};
|
||||
let existingVariableNames = new Set<string>();
|
||||
|
||||
if (await fs.exists(targetPath)) {
|
||||
currentEnvLocal = dotenv.parse(await fs.readFile(targetPath, 'utf8'));
|
||||
if (targetExists) {
|
||||
const currentEnvFile = await fs.readFile(targetPath, 'utf8');
|
||||
({ values: currentEnvLocal, variableNames: existingVariableNames } =
|
||||
getEnvironmentVariableNamesFromEnvFile(currentEnvFile));
|
||||
}
|
||||
|
||||
const filePrefix = `# Environment: ${environment.toLocaleLowerCase()}\n\n`;
|
||||
@@ -103,14 +110,22 @@ export default class EnvPull extends EasCommand {
|
||||
await fs.mkdir(envDir, { recursive: true });
|
||||
}
|
||||
|
||||
const diffLog = await formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables,
|
||||
currentEnvValues: currentEnvLocal,
|
||||
existingVariableNames,
|
||||
envDir,
|
||||
targetExists,
|
||||
});
|
||||
|
||||
const skippedSecretVariables: string[] = [];
|
||||
const overridenSecretVariables: string[] = [];
|
||||
const overriddenSecretVariables: string[] = [];
|
||||
|
||||
const envFileContentLines = await Promise.all(
|
||||
environmentVariables.map(async (variable: EnvironmentVariableWithFileContent) => {
|
||||
if (variable.visibility === EnvironmentVariableVisibility.Secret) {
|
||||
if (currentEnvLocal[variable.name]) {
|
||||
overridenSecretVariables.push(variable.name);
|
||||
if (Object.hasOwn(currentEnvLocal, variable.name)) {
|
||||
overriddenSecretVariables.push(variable.name);
|
||||
return `${variable.name}=${currentEnvLocal[variable.name]}`;
|
||||
}
|
||||
skippedSecretVariables.push(variable.name);
|
||||
@@ -131,9 +146,11 @@ export default class EnvPull extends EasCommand {
|
||||
`Pulled plain text and sensitive environment variables from "${environment.toLowerCase()}" environment to ${targetPath}.`
|
||||
);
|
||||
|
||||
if (overridenSecretVariables.length > 0) {
|
||||
if (overriddenSecretVariables.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`Reused local values for following secrets: ${overridenSecretVariables.join('\n')}.`);
|
||||
Log.log(
|
||||
`Reused local values for following secrets: ${overriddenSecretVariables.join('\n')}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (skippedSecretVariables.length > 0) {
|
||||
@@ -144,5 +161,12 @@ export default class EnvPull extends EasCommand {
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (diffLog.length > 0) {
|
||||
Log.addNewLineIfNone();
|
||||
for (const line of diffLog) {
|
||||
Log.log(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,14 @@ describe(ObserveMetrics, () => {
|
||||
'metric argument is required in non-interactive mode'
|
||||
);
|
||||
});
|
||||
|
||||
it('treats --json as non-interactive when no metric is provided', async () => {
|
||||
const command = createCommand(['--json']);
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'metric argument is required in non-interactive mode'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveOrderBy, () => {
|
||||
|
||||
@@ -1,27 +1,99 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
|
||||
import { fetchObserveSessionEventsAsync } from '../../../observe/fetchSessions';
|
||||
import { ObserveQuery } from '../../../graphql/queries/ObserveQuery';
|
||||
import {
|
||||
fetchObserveSessionEventsAsync,
|
||||
fetchSessionLogCandidatesAsync,
|
||||
fetchSessionMetricCandidatesAsync,
|
||||
} from '../../../observe/fetchSessions';
|
||||
import {
|
||||
buildObserveSessionEventsJson,
|
||||
buildObserveSessionEventsTable,
|
||||
} from '../../../observe/formatSessions';
|
||||
import { selectAsync } from '../../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
import ObserveSession from '../session';
|
||||
|
||||
jest.mock('../../../observe/fetchSessions');
|
||||
jest.mock('../../../observe/formatSessions', () => ({
|
||||
buildObserveSessionEventsTable: jest.fn().mockReturnValue('events-table'),
|
||||
buildObserveSessionEventsJson: jest.fn().mockReturnValue({}),
|
||||
jest.mock('../../../observe/formatSessions', () => {
|
||||
const actual = jest.requireActual('../../../observe/formatSessions');
|
||||
return {
|
||||
...actual,
|
||||
buildObserveSessionEventsTable: jest.fn().mockReturnValue('events-table'),
|
||||
buildObserveSessionEventsJson: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../graphql/queries/ObserveQuery', () => ({
|
||||
ObserveQuery: {
|
||||
customEventNamesAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../../../prompts', () => {
|
||||
const actual = jest.requireActual('../../../prompts');
|
||||
return {
|
||||
...actual,
|
||||
selectAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../utils/json');
|
||||
|
||||
const mockFetchObserveSessionEventsAsync = jest.mocked(fetchObserveSessionEventsAsync);
|
||||
const mockFetchSessionMetricCandidatesAsync = jest.mocked(fetchSessionMetricCandidatesAsync);
|
||||
const mockFetchSessionLogCandidatesAsync = jest.mocked(fetchSessionLogCandidatesAsync);
|
||||
const mockCustomEventNamesAsync = jest.mocked(ObserveQuery.customEventNamesAsync);
|
||||
const mockSelectAsync = jest.mocked(selectAsync);
|
||||
const mockBuildObserveSessionEventsTable = jest.mocked(buildObserveSessionEventsTable);
|
||||
const mockBuildObserveSessionEventsJson = jest.mocked(buildObserveSessionEventsJson);
|
||||
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
|
||||
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
|
||||
|
||||
function makeMetricEvent(overrides: any = {}): any {
|
||||
return {
|
||||
__typename: 'AppObserveEvent',
|
||||
id: 'evt-m-1',
|
||||
metricName: 'expo.app_startup.tti',
|
||||
metricValue: 1.23,
|
||||
timestamp: '2025-01-15T10:00:00.000Z',
|
||||
appVersion: '1.0.0',
|
||||
appBuildNumber: '42',
|
||||
appUpdateId: null,
|
||||
deviceModel: 'iPhone 15',
|
||||
deviceOs: 'iOS',
|
||||
deviceOsVersion: '17.0',
|
||||
countryCode: 'US',
|
||||
sessionId: 'session-metric-1',
|
||||
easClientId: 'client-1',
|
||||
customParams: null,
|
||||
routeName: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCustomEvent(overrides: any = {}): any {
|
||||
return {
|
||||
__typename: 'AppObserveCustomEvent',
|
||||
id: 'evt-c-1',
|
||||
eventName: 'login_pressed',
|
||||
timestamp: '2025-01-15T10:00:00.000Z',
|
||||
sessionId: 'session-log-1',
|
||||
severityNumber: null,
|
||||
severityText: null,
|
||||
appVersion: '1.0.0',
|
||||
appBuildNumber: '42',
|
||||
appUpdateId: null,
|
||||
appEasBuildId: null,
|
||||
deviceModel: 'iPhone 15',
|
||||
deviceOs: 'iOS',
|
||||
deviceOsVersion: '17.0',
|
||||
environment: 'production',
|
||||
easClientId: 'client-1',
|
||||
countryCode: 'US',
|
||||
properties: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe(ObserveSession, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
@@ -35,6 +107,9 @@ describe(ObserveSession, () => {
|
||||
hasMoreMetricEvents: false,
|
||||
hasMoreLogEvents: false,
|
||||
});
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([]);
|
||||
mockFetchSessionLogCandidatesAsync.mockResolvedValue([]);
|
||||
mockCustomEventNamesAsync.mockResolvedValue({ names: [], isTruncated: false });
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): ObserveSession {
|
||||
@@ -47,11 +122,7 @@ describe(ObserveSession, () => {
|
||||
return command;
|
||||
}
|
||||
|
||||
it('requires a session ID argument', async () => {
|
||||
const command = createCommand([]);
|
||||
await expect(command.runAsync()).rejects.toThrow();
|
||||
expect(mockFetchObserveSessionEventsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
// ---- existing single-session behavior ----
|
||||
|
||||
it('passes the session ID argument to the fetcher', async () => {
|
||||
const command = createCommand(['session-abc']);
|
||||
@@ -76,7 +147,7 @@ describe(ObserveSession, () => {
|
||||
expect(mockBuildObserveSessionEventsJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits JSON output when --json is set', async () => {
|
||||
it('emits JSON output when --json is set with a session ID', async () => {
|
||||
const command = createCommand(['session-abc', '--json', '--non-interactive']);
|
||||
await command.runAsync();
|
||||
|
||||
@@ -90,4 +161,212 @@ describe(ObserveSession, () => {
|
||||
const command = createCommand(['session-abc', '--platform', 'ios']);
|
||||
await expect(command.runAsync()).rejects.toThrow();
|
||||
});
|
||||
|
||||
// ---- new picker-mode behavior ----
|
||||
|
||||
it('errors when no session ID is provided in non-interactive mode', async () => {
|
||||
const command = createCommand(['--non-interactive']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/session ID argument is required/);
|
||||
expect(mockFetchObserveSessionEventsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('treats --json as non-interactive (requires a session ID)', async () => {
|
||||
const command = createCommand(['--json']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/session ID argument is required/);
|
||||
expect(mockFetchObserveSessionEventsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects picker flags when a session ID is also provided', async () => {
|
||||
const command = createCommand(['session-abc', '--event-name', 'tti']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/picker flags/);
|
||||
});
|
||||
|
||||
it('rejects --days when a session ID is also provided', async () => {
|
||||
const command = createCommand(['session-abc', '--days', '7']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/picker flags/);
|
||||
});
|
||||
|
||||
it('picker mode with --event-name tti fetches metric events and threads selected sessionId', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([
|
||||
makeMetricEvent({ sessionId: 'picked-session-1' }),
|
||||
]);
|
||||
// 1) sort prompt (no --sort flag), 2) event picker
|
||||
mockSelectAsync.mockResolvedValueOnce('newest').mockResolvedValueOnce('picked-session-1');
|
||||
|
||||
const command = createCommand(['--event-name', 'tti']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockFetchSessionMetricCandidatesAsync).toHaveBeenCalledTimes(1);
|
||||
const options = mockFetchSessionMetricCandidatesAsync.mock.calls[0][2];
|
||||
expect(options.metricName).toBe('expo.app_startup.tti');
|
||||
expect(options.limit).toBe(25);
|
||||
|
||||
expect(mockFetchSessionLogCandidatesAsync).not.toHaveBeenCalled();
|
||||
expect(mockFetchObserveSessionEventsAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
expect.objectContaining({ sessionId: 'picked-session-1' })
|
||||
);
|
||||
});
|
||||
|
||||
it('picker mode with an unknown --event-name treats it as a log event and fetches custom events', async () => {
|
||||
mockFetchSessionLogCandidatesAsync.mockResolvedValue([
|
||||
makeCustomEvent({ sessionId: 'picked-log-session-1' }),
|
||||
]);
|
||||
// 1) sort prompt (no --sort flag), 2) event picker
|
||||
mockSelectAsync.mockResolvedValueOnce('newest').mockResolvedValueOnce('picked-log-session-1');
|
||||
|
||||
const command = createCommand(['--event-name', 'login_pressed']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockFetchSessionLogCandidatesAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockFetchSessionLogCandidatesAsync.mock.calls[0][2].eventName).toBe('login_pressed');
|
||||
expect(mockFetchSessionMetricCandidatesAsync).not.toHaveBeenCalled();
|
||||
expect(mockFetchObserveSessionEventsAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
expect.objectContaining({ sessionId: 'picked-log-session-1' })
|
||||
);
|
||||
});
|
||||
|
||||
it('picker mode without --event-name or --sort prompts for event name, then sort, then event', async () => {
|
||||
mockCustomEventNamesAsync.mockResolvedValue({
|
||||
names: [
|
||||
{ __typename: 'AppObserveCustomEventName', eventName: 'login_pressed', count: 12 } as any,
|
||||
],
|
||||
isTruncated: false,
|
||||
});
|
||||
// 1) event-name picker → metric choice
|
||||
// 2) sort picker → newest
|
||||
// 3) event picker → sessionId
|
||||
mockSelectAsync
|
||||
.mockResolvedValueOnce({ name: 'expo.app_startup.tti', isMetric: true } as any)
|
||||
.mockResolvedValueOnce('newest')
|
||||
.mockResolvedValueOnce('picked-session-2');
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([
|
||||
makeMetricEvent({ sessionId: 'picked-session-2' }),
|
||||
]);
|
||||
|
||||
const command = createCommand([]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockCustomEventNamesAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockSelectAsync).toHaveBeenCalledTimes(3);
|
||||
expect(mockFetchSessionMetricCandidatesAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockFetchSessionMetricCandidatesAsync.mock.calls[0][2].metricName).toBe(
|
||||
'expo.app_startup.tti'
|
||||
);
|
||||
expect(mockFetchObserveSessionEventsAsync.mock.calls[0][2].sessionId).toBe('picked-session-2');
|
||||
});
|
||||
|
||||
it('prompts for sort order when --sort is not provided and offers metric-only options for metric events', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([
|
||||
makeMetricEvent({ sessionId: 'picked-session-3' }),
|
||||
]);
|
||||
mockSelectAsync.mockResolvedValueOnce('slowest').mockResolvedValueOnce('picked-session-3');
|
||||
|
||||
const command = createCommand(['--event-name', 'tti']);
|
||||
await command.runAsync();
|
||||
|
||||
// First selectAsync is the sort picker; verify choice values
|
||||
const sortChoices = mockSelectAsync.mock.calls[0][1];
|
||||
expect(sortChoices.map((c: any) => c.value)).toEqual([
|
||||
'newest',
|
||||
'oldest',
|
||||
'slowest',
|
||||
'fastest',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prompts for sort order without fastest/slowest for log events', async () => {
|
||||
mockFetchSessionLogCandidatesAsync.mockResolvedValue([
|
||||
makeCustomEvent({ sessionId: 'picked-log-session-2' }),
|
||||
]);
|
||||
mockSelectAsync.mockResolvedValueOnce('newest').mockResolvedValueOnce('picked-log-session-2');
|
||||
|
||||
const command = createCommand(['--event-name', 'login_pressed']);
|
||||
await command.runAsync();
|
||||
|
||||
const sortChoices = mockSelectAsync.mock.calls[0][1];
|
||||
expect(sortChoices.map((c: any) => c.value)).toEqual(['newest', 'oldest']);
|
||||
});
|
||||
|
||||
it('skips the sort prompt when --sort is provided explicitly', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([
|
||||
makeMetricEvent({ sessionId: 'picked-session-4' }),
|
||||
]);
|
||||
mockSelectAsync.mockResolvedValueOnce('picked-session-4');
|
||||
|
||||
const command = createCommand(['--event-name', 'tti', '--sort', 'oldest']);
|
||||
await command.runAsync();
|
||||
|
||||
// Only the event picker was called, not the sort picker
|
||||
expect(mockSelectAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('forwards --sort=slowest to the metric candidate fetcher', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([makeMetricEvent()]);
|
||||
mockSelectAsync.mockResolvedValueOnce('session-metric-1');
|
||||
|
||||
const command = createCommand(['--event-name', 'tti', '--sort', 'slowest']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockFetchSessionMetricCandidatesAsync.mock.calls[0][2].sort).toBe('slowest');
|
||||
});
|
||||
|
||||
it('throws when --sort=slowest is combined with a log event name', async () => {
|
||||
const command = createCommand(['--event-name', 'login_pressed', '--sort', 'slowest']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/only supported for metric events/);
|
||||
expect(mockFetchSessionLogCandidatesAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes orderAscending=true to the log candidate fetcher for --sort=oldest', async () => {
|
||||
mockFetchSessionLogCandidatesAsync.mockResolvedValue([makeCustomEvent()]);
|
||||
mockSelectAsync.mockResolvedValueOnce('session-log-1');
|
||||
|
||||
const command = createCommand(['--event-name', 'login_pressed', '--sort', 'oldest']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockFetchSessionLogCandidatesAsync.mock.calls[0][2].orderAscending).toBe(true);
|
||||
});
|
||||
|
||||
it('passes orderAscending=false to the log candidate fetcher for --sort=newest', async () => {
|
||||
mockFetchSessionLogCandidatesAsync.mockResolvedValue([makeCustomEvent()]);
|
||||
mockSelectAsync.mockResolvedValueOnce('session-log-1');
|
||||
|
||||
const command = createCommand(['--event-name', 'login_pressed', '--sort', 'newest']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockFetchSessionLogCandidatesAsync.mock.calls[0][2].orderAscending).toBe(false);
|
||||
});
|
||||
|
||||
it('throws when the metric picker returns no candidate events', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([]);
|
||||
mockSelectAsync.mockResolvedValueOnce('newest');
|
||||
const command = createCommand(['--event-name', 'tti']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/No events found/);
|
||||
expect(mockFetchObserveSessionEventsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('offers each returned candidate as a choice in the event picker', async () => {
|
||||
mockFetchSessionMetricCandidatesAsync.mockResolvedValue([
|
||||
makeMetricEvent({ sessionId: 'session-a' }),
|
||||
makeMetricEvent({ id: 'evt-m-2', sessionId: 'session-b' }),
|
||||
]);
|
||||
// 1) sort prompt, 2) event picker
|
||||
mockSelectAsync.mockResolvedValueOnce('newest').mockResolvedValueOnce('session-a');
|
||||
|
||||
const command = createCommand(['--event-name', 'tti']);
|
||||
await command.runAsync();
|
||||
|
||||
// Second selectAsync call is the event picker
|
||||
const eventChoices = mockSelectAsync.mock.calls[1][1];
|
||||
expect(eventChoices).toHaveLength(2);
|
||||
expect(eventChoices.map((c: any) => c.value)).toEqual(['session-a', 'session-b']);
|
||||
});
|
||||
|
||||
it('rejects --sort when a session ID is also provided', async () => {
|
||||
const command = createCommand(['session-abc', '--sort', 'slowest']);
|
||||
await expect(command.runAsync()).rejects.toThrow(/picker flags/);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
|
||||
import Log from '../../log';
|
||||
import { ObserveQuery } from '../../graphql/queries/ObserveQuery';
|
||||
@@ -73,6 +76,7 @@ export default class ObserveEvents extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(ObserveEvents);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
if (args.eventName && flags['all-events']) {
|
||||
throw new Error(
|
||||
@@ -85,10 +89,10 @@ export default class ObserveEvents extends EasCommand {
|
||||
commandClass: ObserveEvents,
|
||||
loggedInOnlyContextDefinition: ObserveEvents.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
@@ -104,7 +108,7 @@ export default class ObserveEvents extends EasCommand {
|
||||
platform,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildObserveCustomEventNamesJson(names, isTruncated));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -140,7 +144,7 @@ export default class ObserveEvents extends EasCommand {
|
||||
platform,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(
|
||||
buildObserveCustomEventsEmptyWithSuggestionsJson(args.eventName, names, isTruncated)
|
||||
);
|
||||
@@ -158,7 +162,7 @@ export default class ObserveEvents extends EasCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildObserveCustomEventsJson(events, pageInfo));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { fetchObserveMetricsAsync } from '../../observe/fetchMetrics';
|
||||
import {
|
||||
@@ -73,16 +76,17 @@ export default class ObserveMetricsSummary extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ObserveMetricsSummary);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
|
||||
command: this,
|
||||
commandClass: ObserveMetricsSummary,
|
||||
loggedInOnlyContextDefinition: ObserveMetricsSummary.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
@@ -108,7 +112,7 @@ export default class ObserveMetricsSummary extends EasCommand {
|
||||
? Array.from(new Set(flags.stat.map(resolveStatKey)))
|
||||
: undefined;
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
const stats: StatisticKey[] = argumentsStat ?? DEFAULT_STATS_JSON;
|
||||
printJsonOnlyOutput(
|
||||
buildObserveMetricsJson(
|
||||
|
||||
@@ -2,7 +2,10 @@ import { Args, Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasCommandError } from '../../commandUtils/errors';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -71,23 +74,24 @@ export default class ObserveMetrics extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(ObserveMetrics);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
|
||||
command: this,
|
||||
commandClass: ObserveMetrics,
|
||||
loggedInOnlyContextDefinition: ObserveMetrics.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
let metricName: string;
|
||||
if (args.metric) {
|
||||
metricName = resolveMetricName(args.metric);
|
||||
} else if (flags['non-interactive']) {
|
||||
} else if (nonInteractive) {
|
||||
throw new EasCommandError(
|
||||
'A metric argument is required in non-interactive mode. Available metrics: ' +
|
||||
Object.keys(METRIC_ALIASES).join(', ')
|
||||
@@ -128,7 +132,7 @@ export default class ObserveMetrics extends EasCommand {
|
||||
),
|
||||
]);
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildObserveEventsJson(events, pageInfo));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
|
||||
import Log from '../../log';
|
||||
import { fetchObserveNavigationRoutesAsync } from '../../observe/fetchNavigationRoutes';
|
||||
@@ -81,16 +84,17 @@ export default class ObserveRoutes extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ObserveRoutes);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
|
||||
command: this,
|
||||
commandClass: ObserveRoutes,
|
||||
loggedInOnlyContextDefinition: ObserveRoutes.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
@@ -125,7 +129,7 @@ export default class ObserveRoutes extends EasCommand {
|
||||
}
|
||||
);
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
const stats = argumentsStat ?? DEFAULT_STATS_JSON;
|
||||
printJsonOnlyOutput(
|
||||
buildObserveNavigationRoutesJson(routes, metricNames, stats, pageInfoByPlatform)
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import { Args } from '@oclif/core';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { EasCommandError } from '../../commandUtils/errors';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../../log';
|
||||
import { fetchObserveSessionEventsAsync } from '../../observe/fetchSessions';
|
||||
import { ObserveProjectIdFlag } from '../../observe/flags';
|
||||
import { ObserveQuery } from '../../graphql/queries/ObserveQuery';
|
||||
import { EventsOrderPreset } from '../../observe/fetchEvents';
|
||||
import {
|
||||
fetchObserveSessionEventsAsync,
|
||||
fetchSessionLogCandidatesAsync,
|
||||
fetchSessionMetricCandidatesAsync,
|
||||
} from '../../observe/fetchSessions';
|
||||
import { ObserveProjectIdFlag, ObserveTimeRangeFlags } from '../../observe/flags';
|
||||
import {
|
||||
buildObserveSessionEventsJson,
|
||||
buildObserveSessionEventsTable,
|
||||
formatLogCandidateTitle,
|
||||
formatMetricCandidateTitle,
|
||||
} from '../../observe/formatSessions';
|
||||
import {
|
||||
METRIC_SHORT_NAMES,
|
||||
isKnownMetricName,
|
||||
resolveMetricName,
|
||||
} from '../../observe/metricNames';
|
||||
import { resolveObserveCommandContextAsync } from '../../observe/resolveProjectContext';
|
||||
import { resolveTimeRange } from '../../observe/startAndEndTime';
|
||||
import { ExpoChoice, selectAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
// Fixed at 100 — the maximum page size accepted by the underlying events and
|
||||
@@ -17,18 +37,33 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
// command pulls one page of each and merges client-side.
|
||||
const SESSION_PAGE_SIZE = 100;
|
||||
|
||||
// How many candidate events to present in the picker. Small enough to browse;
|
||||
// users narrow further with --days / --sort.
|
||||
const PICKER_CANDIDATE_LIMIT = 25;
|
||||
|
||||
export default class ObserveSession extends EasCommand {
|
||||
static override description =
|
||||
'display the timeline of metric and log events for a specific session';
|
||||
|
||||
static override args = {
|
||||
sessionId: Args.string({
|
||||
description: 'Session ID to inspect',
|
||||
required: true,
|
||||
description: 'Session ID to inspect (omit in interactive mode to pick one from a list)',
|
||||
required: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
sort: Flags.option({
|
||||
description:
|
||||
'Sort order for candidate events when picking a session (if omitted in interactive mode, you will be prompted)',
|
||||
options: Object.values(EventsOrderPreset).map(s => s.toLowerCase()),
|
||||
required: false,
|
||||
})(),
|
||||
'event-name': Flags.string({
|
||||
description:
|
||||
'Metric or log event name to pick candidate sessions by (e.g. tti, cold_launch, login_pressed). If omitted in interactive mode, you will be prompted.',
|
||||
}),
|
||||
...ObserveTimeRangeFlags,
|
||||
...ObserveProjectIdFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
@@ -44,30 +79,59 @@ export default class ObserveSession extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(ObserveSession);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
|
||||
command: this,
|
||||
commandClass: ObserveSession,
|
||||
loggedInOnlyContextDefinition: ObserveSession.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
let sessionId: string;
|
||||
if (args.sessionId) {
|
||||
const pickerFlagsProvided =
|
||||
flags['event-name'] !== undefined ||
|
||||
flags.sort !== undefined ||
|
||||
flags.days !== undefined ||
|
||||
flags.start !== undefined ||
|
||||
flags.end !== undefined;
|
||||
if (pickerFlagsProvided) {
|
||||
throw new EasCommandError(
|
||||
'The picker flags (--event-name, --sort, --days, --start, --end) describe how to find a session and cannot be combined with a session ID argument.'
|
||||
);
|
||||
}
|
||||
sessionId = args.sessionId;
|
||||
} else if (nonInteractive) {
|
||||
throw new EasCommandError(
|
||||
'A session ID argument is required in non-interactive mode. In interactive mode, you can omit the session ID to pick one from a list of events.'
|
||||
);
|
||||
} else {
|
||||
sessionId = await pickSessionIdInteractivelyAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
eventNameFlag: flags['event-name'],
|
||||
sort: flags.sort,
|
||||
timeRangeFlags: { days: flags.days, start: flags.start, end: flags.end },
|
||||
});
|
||||
}
|
||||
|
||||
const { entries, metadata, hasMoreMetricEvents, hasMoreLogEvents } =
|
||||
await fetchObserveSessionEventsAsync(graphqlClient, projectId, {
|
||||
sessionId: args.sessionId,
|
||||
sessionId,
|
||||
limit: SESSION_PAGE_SIZE,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(
|
||||
buildObserveSessionEventsJson(
|
||||
entries,
|
||||
args.sessionId,
|
||||
sessionId,
|
||||
metadata,
|
||||
hasMoreMetricEvents,
|
||||
hasMoreLogEvents
|
||||
@@ -85,3 +149,145 @@ export default class ObserveSession extends EasCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventNameChoice {
|
||||
name: string;
|
||||
isMetric: boolean;
|
||||
}
|
||||
|
||||
async function pickSessionIdInteractivelyAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
eventNameFlag,
|
||||
sort,
|
||||
timeRangeFlags,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
eventNameFlag: string | undefined;
|
||||
sort: string | undefined;
|
||||
timeRangeFlags: { days: number | undefined; start: string | undefined; end: string | undefined };
|
||||
}): Promise<string> {
|
||||
const { startTime, endTime } = resolveTimeRange(timeRangeFlags);
|
||||
|
||||
const eventNameChoice: EventNameChoice = eventNameFlag
|
||||
? { name: eventNameFlag, isMetric: isKnownMetricName(eventNameFlag) }
|
||||
: await promptForEventNameAsync({ graphqlClient, projectId, startTime, endTime });
|
||||
|
||||
let sortValue: string;
|
||||
if (sort) {
|
||||
const preset = sort.toUpperCase() as EventsOrderPreset;
|
||||
const sortIsValueBased =
|
||||
preset === EventsOrderPreset.Slowest || preset === EventsOrderPreset.Fastest;
|
||||
if (!eventNameChoice.isMetric && sortIsValueBased) {
|
||||
throw new EasCommandError(
|
||||
`--sort=${sort} is only supported for metric events. Use newest or oldest for log events.`
|
||||
);
|
||||
}
|
||||
sortValue = sort;
|
||||
} else {
|
||||
sortValue = await promptForSortOrderAsync(eventNameChoice.isMetric);
|
||||
}
|
||||
|
||||
const candidates: CandidateEvent[] = eventNameChoice.isMetric
|
||||
? (
|
||||
await fetchSessionMetricCandidatesAsync(graphqlClient, projectId, {
|
||||
metricName: resolveMetricName(eventNameChoice.name),
|
||||
sort: sortValue,
|
||||
startTime,
|
||||
endTime,
|
||||
limit: PICKER_CANDIDATE_LIMIT,
|
||||
})
|
||||
).map(event => ({ sessionId: event.sessionId, title: formatMetricCandidateTitle(event) }))
|
||||
: (
|
||||
await fetchSessionLogCandidatesAsync(graphqlClient, projectId, {
|
||||
eventName: eventNameChoice.name,
|
||||
orderAscending: sortValue.toUpperCase() === EventsOrderPreset.Oldest,
|
||||
startTime,
|
||||
endTime,
|
||||
limit: PICKER_CANDIDATE_LIMIT,
|
||||
})
|
||||
).map(event => ({ sessionId: event.sessionId, title: formatLogCandidateTitle(event) }));
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new EasCommandError(
|
||||
`No events found for "${eventNameChoice.name}" in the selected time range. Try widening the window with --days or picking a different --event-name.`
|
||||
);
|
||||
}
|
||||
|
||||
return await promptForSessionIdAsync(candidates);
|
||||
}
|
||||
|
||||
async function promptForSortOrderAsync(isMetric: boolean): Promise<string> {
|
||||
const choices: ExpoChoice<string>[] = [
|
||||
{ title: 'Newest first', value: EventsOrderPreset.Newest.valueOf().toLowerCase() },
|
||||
{ title: 'Oldest first', value: EventsOrderPreset.Oldest.valueOf().toLowerCase() },
|
||||
...(isMetric
|
||||
? [
|
||||
{
|
||||
title: 'Slowest first (highest metric value)',
|
||||
value: EventsOrderPreset.Slowest.valueOf().toLowerCase(),
|
||||
},
|
||||
{
|
||||
title: 'Fastest first (lowest metric value)',
|
||||
value: EventsOrderPreset.Fastest.valueOf().toLowerCase(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return await selectAsync('Sort candidate events by', choices);
|
||||
}
|
||||
|
||||
async function promptForEventNameAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}): Promise<EventNameChoice> {
|
||||
const { names: customEventNames } = await ObserveQuery.customEventNamesAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
|
||||
const metricChoices: ExpoChoice<EventNameChoice>[] = Object.entries(METRIC_SHORT_NAMES).map(
|
||||
([fullName, displayName]) => ({
|
||||
title: `${displayName} (metric)`,
|
||||
value: { name: fullName, isMetric: true },
|
||||
})
|
||||
);
|
||||
|
||||
const logChoices: ExpoChoice<EventNameChoice>[] = customEventNames.map(
|
||||
({ eventName, count }) => ({
|
||||
title: `${eventName} (${count} log event${count === 1 ? '' : 's'})`,
|
||||
value: { name: eventName, isMetric: false },
|
||||
})
|
||||
);
|
||||
|
||||
const choices = [...metricChoices, ...logChoices];
|
||||
if (choices.length === 0) {
|
||||
throw new EasCommandError(
|
||||
'No metric or log events found for the selected time range. Widen the window with --days or wait for data to arrive.'
|
||||
);
|
||||
}
|
||||
|
||||
return await selectAsync('Select an event name', choices);
|
||||
}
|
||||
|
||||
interface CandidateEvent {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
async function promptForSessionIdAsync(candidates: CandidateEvent[]): Promise<string> {
|
||||
const choices: ExpoChoice<string>[] = candidates.map(c => ({
|
||||
title: c.title,
|
||||
value: c.sessionId,
|
||||
}));
|
||||
return await selectAsync('Select an event (its session will be shown)', choices);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import Log from '../../log';
|
||||
import { fetchObserveVersionsAsync } from '../../observe/fetchVersions';
|
||||
import {
|
||||
@@ -34,16 +37,17 @@ export default class ObserveVersions extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(ObserveVersions);
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
const { projectId, graphqlClient } = await resolveObserveCommandContextAsync({
|
||||
command: this,
|
||||
commandClass: ObserveVersions,
|
||||
loggedInOnlyContextDefinition: ObserveVersions.loggedInOnlyContextDefinition,
|
||||
projectIdOverride: flags['project-id'],
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
@@ -59,7 +63,7 @@ export default class ObserveVersions extends EasCommand {
|
||||
endTime
|
||||
);
|
||||
|
||||
if (flags.json) {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(buildObserveVersionsJson(results));
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
+824
-71
File diff suppressed because it is too large.
Load diff
@@ -201,6 +201,12 @@ export const AccountQuery = {
|
||||
overageMetrics {
|
||||
id
|
||||
value
|
||||
metadata {
|
||||
... on AccountUsageEASBuildMetadata {
|
||||
billingResourceClass
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCost
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export const DeviceRunSessionQuery = {
|
||||
}
|
||||
... on ServeSimRunSessionRemoteConfig {
|
||||
previewUrl
|
||||
streamUrl
|
||||
}
|
||||
}
|
||||
turtleJobRun {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AppObserveAppVersion,
|
||||
AppObserveCustomEvent,
|
||||
AppObserveCustomEventListFilter,
|
||||
AppObserveCustomEventListOrderBy,
|
||||
AppObserveCustomEventName,
|
||||
AppObserveEvent,
|
||||
AppObserveEventsFilter,
|
||||
@@ -112,6 +113,7 @@ type AppObserveCustomEventListQueryVariables = {
|
||||
filter?: AppObserveCustomEventListFilter;
|
||||
first?: number;
|
||||
after?: string;
|
||||
orderBy?: AppObserveCustomEventListOrderBy;
|
||||
};
|
||||
|
||||
type AppObserveCustomEventNamesQuery = {
|
||||
@@ -328,12 +330,18 @@ export const ObserveQuery = {
|
||||
$filter: AppObserveCustomEventListFilter
|
||||
$first: Int
|
||||
$after: String
|
||||
$orderBy: AppObserveCustomEventListOrderBy
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
observe {
|
||||
customEventList(filter: $filter, first: $first, after: $after) {
|
||||
customEventList(
|
||||
filter: $filter
|
||||
first: $first
|
||||
after: $after
|
||||
orderBy: $orderBy
|
||||
) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import {
|
||||
AppObserveCustomEvent,
|
||||
AppObserveCustomEventListOrderByField,
|
||||
AppObserveEvent,
|
||||
AppObserveEventsOrderByDirection,
|
||||
AppObserveEventsOrderByField,
|
||||
} from '../../graphql/generated';
|
||||
import { fetchObserveCustomEventsAsync } from '../fetchCustomEvents';
|
||||
import { fetchObserveEventsAsync } from '../fetchEvents';
|
||||
import { fetchObserveSessionEventsAsync } from '../fetchSessions';
|
||||
import {
|
||||
fetchObserveSessionEventsAsync,
|
||||
fetchSessionLogCandidatesAsync,
|
||||
fetchSessionMetricCandidatesAsync,
|
||||
} from '../fetchSessions';
|
||||
|
||||
jest.mock('../fetchCustomEvents');
|
||||
jest.mock('../fetchEvents');
|
||||
jest.mock('../fetchEvents', () => {
|
||||
const actual = jest.requireActual('../fetchEvents');
|
||||
return {
|
||||
...actual,
|
||||
fetchObserveEventsAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockFetchObserveEventsAsync = jest.mocked(fetchObserveEventsAsync);
|
||||
const mockFetchObserveCustomEventsAsync = jest.mocked(fetchObserveCustomEventsAsync);
|
||||
@@ -191,3 +202,149 @@ describe('fetchObserveSessionEventsAsync', () => {
|
||||
expect(result.hasMoreLogEvents).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSessionMetricCandidatesAsync', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFetchObserveEventsAsync.mockResolvedValue({
|
||||
events: [],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards metricName, sort, window, and limit into fetchObserveEventsAsync', async () => {
|
||||
await fetchSessionMetricCandidatesAsync({} as any, 'project-1', {
|
||||
metricName: 'expo.app_startup.tti',
|
||||
sort: 'slowest',
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
|
||||
const options = mockFetchObserveEventsAsync.mock.calls[0][2];
|
||||
expect(options.metricName).toBe('expo.app_startup.tti');
|
||||
expect(options.limit).toBe(25);
|
||||
expect(options.startTime).toBe('2025-01-01T00:00:00.000Z');
|
||||
expect(options.endTime).toBe('2025-02-01T00:00:00.000Z');
|
||||
expect(options.orderBy).toEqual({
|
||||
field: AppObserveEventsOrderByField.MetricValue,
|
||||
direction: AppObserveEventsOrderByDirection.Desc,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out events without a sessionId', async () => {
|
||||
mockFetchObserveEventsAsync.mockResolvedValue({
|
||||
events: [
|
||||
makeMetricEvent({ id: 'evt-1', sessionId: 'session-a' }),
|
||||
makeMetricEvent({ id: 'evt-2', sessionId: null }),
|
||||
makeMetricEvent({ id: 'evt-3', sessionId: 'session-b' }),
|
||||
],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
});
|
||||
|
||||
const result = await fetchSessionMetricCandidatesAsync({} as any, 'project-1', {
|
||||
metricName: 'expo.app_startup.tti',
|
||||
sort: 'newest',
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
|
||||
expect(result.map(e => e.id)).toEqual(['evt-1', 'evt-3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSessionLogCandidatesAsync', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFetchObserveCustomEventsAsync.mockResolvedValue({
|
||||
events: [],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards eventName, window, and limit into fetchObserveCustomEventsAsync', async () => {
|
||||
await fetchSessionLogCandidatesAsync({} as any, 'project-1', {
|
||||
eventName: 'login_pressed',
|
||||
orderAscending: false,
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
|
||||
const options = mockFetchObserveCustomEventsAsync.mock.calls[0][2];
|
||||
expect(options.eventName).toBe('login_pressed');
|
||||
expect(options.limit).toBe(25);
|
||||
expect(options.startTime).toBe('2025-01-01T00:00:00.000Z');
|
||||
expect(options.endTime).toBe('2025-02-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('requests descending timestamp order when orderAscending is false', async () => {
|
||||
await fetchSessionLogCandidatesAsync({} as any, 'project-1', {
|
||||
eventName: 'login_pressed',
|
||||
orderAscending: false,
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
|
||||
expect(mockFetchObserveCustomEventsAsync.mock.calls[0][2].orderBy).toEqual({
|
||||
field: AppObserveCustomEventListOrderByField.Timestamp,
|
||||
direction: AppObserveEventsOrderByDirection.Desc,
|
||||
});
|
||||
});
|
||||
|
||||
it('requests ascending timestamp order when orderAscending is true', async () => {
|
||||
await fetchSessionLogCandidatesAsync({} as any, 'project-1', {
|
||||
eventName: 'login_pressed',
|
||||
orderAscending: true,
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
|
||||
expect(mockFetchObserveCustomEventsAsync.mock.calls[0][2].orderBy).toEqual({
|
||||
field: AppObserveCustomEventListOrderByField.Timestamp,
|
||||
direction: AppObserveEventsOrderByDirection.Asc,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves the server-provided order (no client-side re-sorting)', async () => {
|
||||
mockFetchObserveCustomEventsAsync.mockResolvedValue({
|
||||
events: [
|
||||
makeCustomEvent({ id: 'c', timestamp: '2025-01-15T10:10:00.000Z' }),
|
||||
makeCustomEvent({ id: 'b', timestamp: '2025-01-15T10:05:00.000Z' }),
|
||||
makeCustomEvent({ id: 'a', timestamp: '2025-01-15T10:00:00.000Z' }),
|
||||
],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
});
|
||||
|
||||
const result = await fetchSessionLogCandidatesAsync({} as any, 'project-1', {
|
||||
eventName: 'login_pressed',
|
||||
orderAscending: false,
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
expect(result.map(e => e.id)).toEqual(['c', 'b', 'a']);
|
||||
});
|
||||
|
||||
it('filters out events without a sessionId', async () => {
|
||||
mockFetchObserveCustomEventsAsync.mockResolvedValue({
|
||||
events: [
|
||||
makeCustomEvent({ id: 'a', sessionId: 'session-a' }),
|
||||
makeCustomEvent({ id: 'b', sessionId: null }),
|
||||
],
|
||||
pageInfo: { hasNextPage: false, hasPreviousPage: false },
|
||||
});
|
||||
|
||||
const result = await fetchSessionLogCandidatesAsync({} as any, 'project-1', {
|
||||
eventName: 'login_pressed',
|
||||
orderAscending: false,
|
||||
startTime: '2025-01-01T00:00:00.000Z',
|
||||
endTime: '2025-02-01T00:00:00.000Z',
|
||||
limit: 25,
|
||||
});
|
||||
expect(result.map(e => e.id)).toEqual(['a']);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,12 @@
|
||||
import { AppObserveCustomEvent, AppObserveEvent } from '../../graphql/generated';
|
||||
import { SessionEventEntry, SessionMetadata } from '../fetchSessions';
|
||||
import { buildObserveSessionEventsJson, buildObserveSessionEventsTable } from '../formatSessions';
|
||||
import {
|
||||
buildObserveSessionEventsJson,
|
||||
buildObserveSessionEventsTable,
|
||||
formatLogCandidateTitle,
|
||||
formatMetricCandidateTitle,
|
||||
shortSessionId,
|
||||
} from '../formatSessions';
|
||||
|
||||
function makeMetricEntry(overrides: Partial<SessionEventEntry> = {}): SessionEventEntry {
|
||||
return {
|
||||
@@ -240,3 +247,91 @@ describe(buildObserveSessionEventsJson, () => {
|
||||
expect(result.metadata).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(shortSessionId, () => {
|
||||
it('returns the full id when it is 12 characters or fewer', () => {
|
||||
expect(shortSessionId('abc')).toBe('abc');
|
||||
expect(shortSessionId('abcdefghijkl')).toBe('abcdefghijkl');
|
||||
});
|
||||
|
||||
it('collapses long ids to <first-8>…<last-4>', () => {
|
||||
expect(shortSessionId('abcdefgh1234567890zyxw')).toBe('abcdefgh…zyxw');
|
||||
});
|
||||
});
|
||||
|
||||
function makeMetricEvent(overrides: Partial<AppObserveEvent> = {}): AppObserveEvent {
|
||||
return {
|
||||
__typename: 'AppObserveEvent' as const,
|
||||
id: 'evt-m-1',
|
||||
metricName: 'expo.app_startup.tti',
|
||||
metricValue: 1.23,
|
||||
timestamp: '2025-01-15T10:00:00.000Z',
|
||||
appVersion: '1.0.0',
|
||||
appBuildNumber: '42',
|
||||
appUpdateId: null,
|
||||
deviceModel: 'iPhone 15',
|
||||
deviceOs: 'iOS',
|
||||
deviceOsVersion: '17.0',
|
||||
countryCode: 'US',
|
||||
sessionId: 'session-abcdefgh1234567890',
|
||||
easClientId: 'client-1',
|
||||
customParams: null,
|
||||
routeName: null,
|
||||
...overrides,
|
||||
} as AppObserveEvent;
|
||||
}
|
||||
|
||||
function makeCustomEventEvt(overrides: Partial<AppObserveCustomEvent> = {}): AppObserveCustomEvent {
|
||||
return {
|
||||
__typename: 'AppObserveCustomEvent' as const,
|
||||
id: 'evt-c-1',
|
||||
eventName: 'login_pressed',
|
||||
timestamp: '2025-01-15T10:00:00.000Z',
|
||||
sessionId: 'session-abcdefgh1234567890',
|
||||
severityNumber: null,
|
||||
severityText: null,
|
||||
appVersion: '1.0.0',
|
||||
appBuildNumber: '42',
|
||||
appUpdateId: null,
|
||||
appEasBuildId: null,
|
||||
deviceModel: 'iPhone 15',
|
||||
deviceOs: 'iOS',
|
||||
deviceOsVersion: '17.0',
|
||||
environment: 'production',
|
||||
easClientId: 'client-1',
|
||||
countryCode: 'US',
|
||||
properties: [],
|
||||
...overrides,
|
||||
} as AppObserveCustomEvent;
|
||||
}
|
||||
|
||||
describe(formatMetricCandidateTitle, () => {
|
||||
it('includes timestamp, display name, value, version, device, and truncated session id', () => {
|
||||
const title = formatMetricCandidateTitle(makeMetricEvent());
|
||||
expect(title).toContain('Startup TTI 1.23s');
|
||||
expect(title).toContain('1.0.0');
|
||||
expect(title).toContain('iOS 17.0');
|
||||
expect(title).toContain('session session-…7890');
|
||||
});
|
||||
});
|
||||
|
||||
describe(formatLogCandidateTitle, () => {
|
||||
it('includes timestamp, event name, and severity (using severityText when set)', () => {
|
||||
const title = formatLogCandidateTitle(makeCustomEventEvt({ severityText: 'WARN' }));
|
||||
expect(title).toContain('login_pressed');
|
||||
expect(title).toContain('WARN');
|
||||
expect(title).toContain('1.0.0');
|
||||
});
|
||||
|
||||
it('falls back to severityNumber when severityText is null', () => {
|
||||
const title = formatLogCandidateTitle(
|
||||
makeCustomEventEvt({ severityText: null, severityNumber: 13 })
|
||||
);
|
||||
expect(title).toContain('13');
|
||||
});
|
||||
|
||||
it('shows "-" for severity when both fields are null', () => {
|
||||
const title = formatLogCandidateTitle(makeCustomEventEvt());
|
||||
expect(title).toContain('login_pressed · -');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
getMetricDisplayName,
|
||||
isKnownMetricName,
|
||||
resolveMetricName,
|
||||
resolveNavigationMetricName,
|
||||
} from '../metricNames';
|
||||
@@ -110,3 +111,31 @@ describe(getMetricDisplayName, () => {
|
||||
expect(getMetricDisplayName('custom.metric.name')).toBe('custom.metric.name');
|
||||
});
|
||||
});
|
||||
|
||||
describe(isKnownMetricName, () => {
|
||||
it('returns true for app-startup short aliases', () => {
|
||||
expect(isKnownMetricName('tti')).toBe(true);
|
||||
expect(isKnownMetricName('cold_launch')).toBe(true);
|
||||
expect(isKnownMetricName('bundle_load')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for navigation short aliases', () => {
|
||||
expect(isKnownMetricName('nav_cold_ttr')).toBe(true);
|
||||
expect(isKnownMetricName('nav_warm_ttr')).toBe(true);
|
||||
expect(isKnownMetricName('nav_tti')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for full metric names', () => {
|
||||
expect(isKnownMetricName('expo.app_startup.tti')).toBe(true);
|
||||
expect(isKnownMetricName('expo.navigation.cold_ttr')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unknown names (including custom event names)', () => {
|
||||
expect(isKnownMetricName('login_pressed')).toBe(false);
|
||||
expect(isKnownMetricName('some_random_event')).toBe(false);
|
||||
expect(isKnownMetricName('')).toBe(false);
|
||||
// A dotted name that isn't in the known-full set is still not "known"
|
||||
// for picker purposes; the picker treats it as a log event.
|
||||
expect(isKnownMetricName('custom.metric.name')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
|
||||
import {
|
||||
AppObserveCustomEvent,
|
||||
AppObserveCustomEventListFilter,
|
||||
AppObserveCustomEventListOrderBy,
|
||||
AppObservePlatform,
|
||||
PageInfo,
|
||||
} from '../graphql/generated';
|
||||
@@ -17,6 +18,7 @@ interface FetchCustomEventsOptions {
|
||||
appVersion?: string;
|
||||
updateId?: string;
|
||||
sessionId?: string;
|
||||
orderBy?: AppObserveCustomEventListOrderBy;
|
||||
}
|
||||
|
||||
interface FetchCustomEventsResult {
|
||||
@@ -44,5 +46,6 @@ export async function fetchObserveCustomEventsAsync(
|
||||
filter,
|
||||
first: options.limit,
|
||||
...(options.after && { after: options.after }),
|
||||
...(options.orderBy && { orderBy: options.orderBy }),
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AppObserveCustomEvent,
|
||||
AppObserveCustomEventListOrderByField,
|
||||
AppObserveEvent,
|
||||
AppObserveEventsOrderByDirection,
|
||||
AppObserveEventsOrderByField,
|
||||
} from '../graphql/generated';
|
||||
import { fetchObserveCustomEventsAsync } from './fetchCustomEvents';
|
||||
import { fetchObserveEventsAsync } from './fetchEvents';
|
||||
import { fetchObserveEventsAsync, resolveOrderBy } from './fetchEvents';
|
||||
|
||||
export interface SessionEventEntry {
|
||||
source: 'metric' | 'log';
|
||||
@@ -146,3 +147,77 @@ export async function fetchObserveSessionEventsAsync(
|
||||
hasMoreLogEvents: logResult.pageInfo.hasNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A metric event that is guaranteed to belong to a session — used as a
|
||||
* candidate when picking a session to inspect via `observe:session`.
|
||||
*/
|
||||
export type SessionMetricCandidate = AppObserveEvent & { sessionId: string };
|
||||
|
||||
export interface FetchSessionMetricCandidatesOptions {
|
||||
metricName: string;
|
||||
/** One of EventsOrderPreset (case-insensitive). */
|
||||
sort: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a page of metric events for the given metricName + window, ordered
|
||||
* per `sort`, and filtered to events that have a sessionId. The events query
|
||||
* supports server-side ordering, so `sort` is passed straight through.
|
||||
*/
|
||||
export async function fetchSessionMetricCandidatesAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
options: FetchSessionMetricCandidatesOptions
|
||||
): Promise<SessionMetricCandidate[]> {
|
||||
const { events } = await fetchObserveEventsAsync(graphqlClient, appId, {
|
||||
metricName: options.metricName,
|
||||
orderBy: resolveOrderBy(options.sort),
|
||||
limit: options.limit,
|
||||
startTime: options.startTime,
|
||||
endTime: options.endTime,
|
||||
});
|
||||
return events.filter((e): e is SessionMetricCandidate => !!e.sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom log event that is guaranteed to belong to a session — used as a
|
||||
* candidate when picking a session to inspect via `observe:session`.
|
||||
*/
|
||||
export type SessionLogCandidate = AppObserveCustomEvent & { sessionId: string };
|
||||
|
||||
export interface FetchSessionLogCandidatesOptions {
|
||||
eventName: string;
|
||||
/** True → oldest-first (ascending timestamp); false → newest-first. */
|
||||
orderAscending: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a page of custom log events for the given eventName + window, ordered
|
||||
* by timestamp server-side, and filtered to events that have a sessionId.
|
||||
*/
|
||||
export async function fetchSessionLogCandidatesAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
options: FetchSessionLogCandidatesOptions
|
||||
): Promise<SessionLogCandidate[]> {
|
||||
const { events } = await fetchObserveCustomEventsAsync(graphqlClient, appId, {
|
||||
eventName: options.eventName,
|
||||
limit: options.limit,
|
||||
startTime: options.startTime,
|
||||
endTime: options.endTime,
|
||||
orderBy: {
|
||||
field: AppObserveCustomEventListOrderByField.Timestamp,
|
||||
direction: options.orderAscending
|
||||
? AppObserveEventsOrderByDirection.Asc
|
||||
: AppObserveEventsOrderByDirection.Desc,
|
||||
},
|
||||
});
|
||||
return events.filter((e): e is SessionLogCandidate => !!e.sessionId);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { AppObserveCustomEvent, AppObserveEvent } from '../graphql/generated';
|
||||
import { SessionEventEntry, SessionMetadata } from './fetchSessions';
|
||||
import { formatLogTimestamp } from './formatUtils';
|
||||
import { getMetricDisplayName } from './metricNames';
|
||||
@@ -142,3 +143,40 @@ export function buildObserveSessionEventsJson(
|
||||
hasMoreLogEvents,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact session ID for display in candidate lists — long UUIDs are
|
||||
* truncated to `<first-8>…<last-4>` so a row stays scannable.
|
||||
*/
|
||||
export function shortSessionId(sessionId: string): string {
|
||||
if (sessionId.length <= 12) {
|
||||
return sessionId;
|
||||
}
|
||||
return `${sessionId.slice(0, 8)}…${sessionId.slice(-4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-line title for a metric event shown in the observe:session candidate
|
||||
* picker, e.g. `Jan 15, 10:00:00.000 AM · Startup TTI 1.23s · 1.0.0 · iOS 17.0 · session abc…1234`.
|
||||
*/
|
||||
export function formatMetricCandidateTitle(event: AppObserveEvent): string {
|
||||
const displayName = getMetricDisplayName(event.metricName);
|
||||
const value = `${event.metricValue.toFixed(2)}s`;
|
||||
const timestamp = formatLogTimestamp(event.timestamp);
|
||||
const shortSession = shortSessionId(event.sessionId ?? '');
|
||||
const device = `${event.deviceOs} ${event.deviceOsVersion}`;
|
||||
return `${timestamp} · ${displayName} ${value} · ${event.appVersion} · ${device} · session ${shortSession}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-line title for a custom log event shown in the observe:session
|
||||
* candidate picker.
|
||||
*/
|
||||
export function formatLogCandidateTitle(event: AppObserveCustomEvent): string {
|
||||
const timestamp = formatLogTimestamp(event.timestamp);
|
||||
const severity =
|
||||
event.severityText ?? (event.severityNumber != null ? String(event.severityNumber) : '-');
|
||||
const shortSession = shortSessionId(event.sessionId ?? '');
|
||||
const device = `${event.deviceOs} ${event.deviceOsVersion}`;
|
||||
return `${timestamp} · ${event.eventName} · ${severity} · ${event.appVersion} · ${device} · session ${shortSession}`;
|
||||
}
|
||||
@@ -60,3 +60,18 @@ export function resolveNavigationMetricName(input: string): string {
|
||||
export function getMetricDisplayName(metricName: string): string {
|
||||
return METRIC_SHORT_NAMES[metricName] ?? metricName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-throwing predicate: true when `input` is a known metric alias (either
|
||||
* app-startup or navigation) or a known full metric name. Used to decide
|
||||
* whether an event name should be treated as a metric or as a custom log
|
||||
* event without forcing the caller into a try/catch around `resolveMetricName`.
|
||||
*/
|
||||
export function isKnownMetricName(input: string): boolean {
|
||||
return (
|
||||
input in METRIC_ALIASES ||
|
||||
input in NAVIGATION_METRIC_ALIASES ||
|
||||
KNOWN_FULL_NAMES.has(input) ||
|
||||
KNOWN_FULL_NAVIGATION_NAMES.has(input)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
EnvironmentVariableVisibility,
|
||||
} from '../../graphql/generated';
|
||||
import { EnvironmentVariableWithFileContent } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import {
|
||||
formatEnvironmentVariableDiffAsync,
|
||||
getEnvironmentVariableNamesFromEnvFile,
|
||||
} from '../environmentVariableDiff';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
|
||||
const envDir = '/project/.eas/.env';
|
||||
|
||||
function variable(
|
||||
name: string,
|
||||
value: string | null,
|
||||
overrides: Partial<EnvironmentVariableWithFileContent> = {}
|
||||
): EnvironmentVariableWithFileContent {
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
value,
|
||||
environments: ['development'],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe(formatEnvironmentVariableDiffAsync, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('renders a fresh pull as a neutral baseline', async () => {
|
||||
const result = await formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [variable('ONE', '1'), variable('TWO', '2')],
|
||||
currentEnvValues: {},
|
||||
existingVariableNames: new Set(),
|
||||
envDir,
|
||||
targetExists: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual([' ONE', ' TWO']);
|
||||
});
|
||||
|
||||
it('renders variables as added when the target file exists but is empty', async () => {
|
||||
const result = await formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [variable('ONE', '1')],
|
||||
currentEnvValues: {},
|
||||
existingVariableNames: new Set(),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([chalk.green('+ ONE')]);
|
||||
});
|
||||
|
||||
it('renders added, changed, unchanged, and removed string variables', async () => {
|
||||
const result = await formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [
|
||||
variable('ADDED', 'new'),
|
||||
variable('CHANGED', 'new'),
|
||||
variable('UNCHANGED', 'same'),
|
||||
],
|
||||
currentEnvValues: { CHANGED: 'old', UNCHANGED: 'same', REMOVED: 'old' },
|
||||
existingVariableNames: new Set(['CHANGED', 'UNCHANGED', 'REMOVED']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
chalk.green('+ ADDED'),
|
||||
chalk.yellow('~ CHANGED'),
|
||||
' UNCHANGED',
|
||||
chalk.red('- REMOVED'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps existing secrets neutral without claiming their values match', async () => {
|
||||
const secret = variable('SECRET', null, {
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
});
|
||||
|
||||
const result = await formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [
|
||||
secret,
|
||||
variable('NEW_SECRET', null, {
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
}),
|
||||
],
|
||||
currentEnvValues: {},
|
||||
existingVariableNames: new Set(['SECRET']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([' SECRET', chalk.green('+ NEW_SECRET')]);
|
||||
});
|
||||
|
||||
it('only considers a file unchanged when its output path and contents match', async () => {
|
||||
const fileContents = Buffer.from('contents').toString('base64');
|
||||
const fileVariable = variable('FILE', null, {
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
valueWithFileContent: fileContents,
|
||||
});
|
||||
const expectedFilePath = path.join(envDir, fileVariable.name);
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.pathExists).mockResolvedValue(true);
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.readFile).mockResolvedValue(fileContents);
|
||||
|
||||
await expect(
|
||||
formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [fileVariable],
|
||||
currentEnvValues: { FILE: expectedFilePath },
|
||||
existingVariableNames: new Set(['FILE']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
})
|
||||
).resolves.toEqual([' FILE']);
|
||||
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.readFile).mockResolvedValue(Buffer.from('different').toString('base64'));
|
||||
await expect(
|
||||
formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [fileVariable],
|
||||
currentEnvValues: { FILE: expectedFilePath },
|
||||
existingVariableNames: new Set(['FILE']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
})
|
||||
).resolves.toEqual([chalk.yellow('~ FILE')]);
|
||||
|
||||
await expect(
|
||||
formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables: [fileVariable],
|
||||
currentEnvValues: { FILE: '/some/other/file' },
|
||||
existingVariableNames: new Set(['FILE']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
})
|
||||
).resolves.toEqual([chalk.yellow('~ FILE')]);
|
||||
});
|
||||
|
||||
it('reports missing or unreadable file variables as changed instead of failing', async () => {
|
||||
const fileVariable = variable('FILE', null, {
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
valueWithFileContent: Buffer.from('contents').toString('base64'),
|
||||
});
|
||||
const expectedFilePath = path.join(envDir, fileVariable.name);
|
||||
const options = {
|
||||
environmentVariables: [fileVariable],
|
||||
currentEnvValues: { FILE: expectedFilePath },
|
||||
existingVariableNames: new Set(['FILE']),
|
||||
envDir,
|
||||
targetExists: true,
|
||||
};
|
||||
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.pathExists).mockResolvedValue(false);
|
||||
await expect(formatEnvironmentVariableDiffAsync(options)).resolves.toEqual([
|
||||
chalk.yellow('~ FILE'),
|
||||
]);
|
||||
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.pathExists).mockResolvedValue(true);
|
||||
// @ts-expect-error fs-extra overloads are not inferred correctly by Jest.
|
||||
jest.mocked(fs.readFile).mockRejectedValue(new Error('EACCES'));
|
||||
await expect(formatEnvironmentVariableDiffAsync(options)).resolves.toEqual([
|
||||
chalk.yellow('~ FILE'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getEnvironmentVariableNamesFromEnvFile, () => {
|
||||
it('includes secret placeholders in the existing variable names', () => {
|
||||
const result = getEnvironmentVariableNamesFromEnvFile(
|
||||
['PLAIN=value', '# SECRET=***** (secret)', '# unrelated comment'].join('\n')
|
||||
);
|
||||
|
||||
expect(result.values).toEqual({ PLAIN: 'value' });
|
||||
expect(result.variableNames).toEqual(new Set(['PLAIN', 'SECRET']));
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,13 @@
|
||||
import { randomInt } from 'crypto';
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
|
||||
import { ImageNonPngError, ImageTransparencyError, ensurePNGIsNotTransparentAsync } from '../image';
|
||||
|
||||
const TEST_SERVER_PORT = randomInt(9000, 9000 + 1000);
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
const transparentPngPath = path.join(fixturesPath, 'icon-alpha.png');
|
||||
const nonTransparentPngPath = path.join(fixturesPath, 'icon-no-alpha.png');
|
||||
const jpgPath = path.join(fixturesPath, 'icon.jpg');
|
||||
const nonTransparentPngURL = `http://127.0.0.1:${TEST_SERVER_PORT}/icon-alpha.png`;
|
||||
|
||||
describe(ensurePNGIsNotTransparentAsync, () => {
|
||||
describe('local paths', () => {
|
||||
@@ -32,15 +28,27 @@ describe(ensurePNGIsNotTransparentAsync, () => {
|
||||
|
||||
describe('remote URLs', () => {
|
||||
let server: http.Server;
|
||||
let transparentPngURL: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await new Promise<void>(res => {
|
||||
const app = express();
|
||||
app.use(express.static(fixturesPath));
|
||||
server = app.listen(TEST_SERVER_PORT, () => {
|
||||
server = http.createServer((_request, response) => {
|
||||
response.setHeader('Content-Type', 'image/png');
|
||||
fs.createReadStream(transparentPngPath).pipe(response);
|
||||
});
|
||||
await new Promise<void>((res, rej) => {
|
||||
const onError = (error: Error): void => rej(error);
|
||||
server.once('error', onError);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', onError);
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to determine test server address');
|
||||
}
|
||||
transparentPngURL = `http://127.0.0.1:${address.port}/icon-alpha.png`;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -52,7 +60,7 @@ describe(ensurePNGIsNotTransparentAsync, () => {
|
||||
});
|
||||
|
||||
it('works with remote URLs', async () => {
|
||||
await expect(ensurePNGIsNotTransparentAsync(nonTransparentPngURL)).rejects.toThrowError(
|
||||
await expect(ensurePNGIsNotTransparentAsync(transparentPngURL)).rejects.toThrowError(
|
||||
ImageTransparencyError
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../graphql/generated';
|
||||
import { EnvironmentVariableWithFileContent } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
|
||||
interface EnvironmentVariableDiffOptions {
|
||||
environmentVariables: EnvironmentVariableWithFileContent[];
|
||||
currentEnvValues: Record<string, string>;
|
||||
existingVariableNames: Set<string>;
|
||||
envDir: string;
|
||||
targetExists: boolean;
|
||||
}
|
||||
|
||||
export function getEnvironmentVariableNamesFromEnvFile(contents: string): {
|
||||
values: Record<string, string>;
|
||||
variableNames: Set<string>;
|
||||
} {
|
||||
const values = dotenv.parse(contents);
|
||||
const variableNames = new Set(Object.keys(values));
|
||||
const secretPlaceholderPattern = /^#\s+([^=\s]+)=\*{5}\s+\(secret\)\s*$/gm;
|
||||
|
||||
for (const match of contents.matchAll(secretPlaceholderPattern)) {
|
||||
variableNames.add(match[1]);
|
||||
}
|
||||
|
||||
return { values, variableNames };
|
||||
}
|
||||
|
||||
async function isFileVariableUnchangedAsync(
|
||||
currentEnvValue: string | undefined,
|
||||
newVariable: EnvironmentVariableWithFileContent,
|
||||
envDir: string
|
||||
): Promise<boolean> {
|
||||
const expectedFilePath = path.join(envDir, newVariable.name);
|
||||
if (currentEnvValue !== expectedFilePath || !newVariable.valueWithFileContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await fs.pathExists(currentEnvValue))) {
|
||||
return false;
|
||||
}
|
||||
return (await fs.readFile(currentEnvValue, 'base64')) === newVariable.valueWithFileContent;
|
||||
} catch {
|
||||
// Diff rendering should never prevent env:pull from replacing an unreadable file.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isEnvironmentVariableUnchangedAsync(
|
||||
currentEnvValue: string | undefined,
|
||||
newVariable: EnvironmentVariableWithFileContent,
|
||||
envDir: string
|
||||
): Promise<boolean> {
|
||||
if (newVariable.visibility === EnvironmentVariableVisibility.Secret) {
|
||||
// env:pull preserves an existing local secret; it cannot compare it with the remote value.
|
||||
return true;
|
||||
}
|
||||
if (newVariable.type === EnvironmentSecretType.FileBase64) {
|
||||
return await isFileVariableUnchangedAsync(currentEnvValue, newVariable, envDir);
|
||||
}
|
||||
return currentEnvValue === newVariable.value;
|
||||
}
|
||||
|
||||
export async function formatEnvironmentVariableDiffAsync({
|
||||
environmentVariables,
|
||||
currentEnvValues,
|
||||
existingVariableNames,
|
||||
envDir,
|
||||
targetExists,
|
||||
}: EnvironmentVariableDiffOptions): Promise<string[]> {
|
||||
if (!targetExists) {
|
||||
return environmentVariables.map(variable => ` ${variable.name}`);
|
||||
}
|
||||
|
||||
const newVariablesByName = new Map(
|
||||
environmentVariables.map(variable => [variable.name, variable])
|
||||
);
|
||||
const diffLog: string[] = [];
|
||||
|
||||
for (const variable of environmentVariables) {
|
||||
if (!existingVariableNames.has(variable.name)) {
|
||||
diffLog.push(chalk.green(`+ ${variable.name}`));
|
||||
} else if (
|
||||
await isEnvironmentVariableUnchangedAsync(currentEnvValues[variable.name], variable, envDir)
|
||||
) {
|
||||
diffLog.push(` ${variable.name}`);
|
||||
} else {
|
||||
diffLog.push(chalk.yellow(`~ ${variable.name}`));
|
||||
}
|
||||
}
|
||||
|
||||
for (const variableName of existingVariableNames) {
|
||||
if (!newVariablesByName.has(variableName)) {
|
||||
diffLog.push(chalk.red(`- ${variableName}`));
|
||||
}
|
||||
}
|
||||
|
||||
return diffLog;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AccountUsageForOverageWarningQuery,
|
||||
AppPlatform,
|
||||
EasBuildBillingResourceClass,
|
||||
EasService,
|
||||
EasServiceMetric,
|
||||
EstimatedUsage,
|
||||
@@ -40,6 +42,30 @@ function createMockPlanMetric({
|
||||
};
|
||||
}
|
||||
|
||||
type MockOverageMetric =
|
||||
AccountUsageForOverageWarningQuery['account']['byId']['usageMetrics']['EAS_BUILD']['overageMetrics'][number];
|
||||
|
||||
// A genuine per-worker build overage row, i.e. one carrying build metadata. Only rows
|
||||
// shaped like this should be counted toward the "N builds beyond your credits" total.
|
||||
function createMockBuildOverage({
|
||||
id = 'overage-id',
|
||||
value = 1,
|
||||
platform = AppPlatform.Ios,
|
||||
billingResourceClass = EasBuildBillingResourceClass.Medium,
|
||||
}: {
|
||||
id?: string;
|
||||
value?: number;
|
||||
platform?: AppPlatform;
|
||||
billingResourceClass?: EasBuildBillingResourceClass;
|
||||
} = {}): MockOverageMetric {
|
||||
return {
|
||||
__typename: 'EstimatedOverageAndCost',
|
||||
id,
|
||||
value,
|
||||
metadata: { __typename: 'AccountUsageEASBuildMetadata', billingResourceClass, platform },
|
||||
};
|
||||
}
|
||||
|
||||
function createMockAccountUsage({
|
||||
id = 'account-id',
|
||||
name = 'test-account',
|
||||
@@ -52,7 +78,7 @@ function createMockAccountUsage({
|
||||
name?: string;
|
||||
subscriptionName?: string;
|
||||
buildPlanMetrics?: EstimatedUsage[];
|
||||
overageMetrics?: { __typename: 'EstimatedOverageAndCost'; id: string; value: number }[];
|
||||
overageMetrics?: MockOverageMetric[];
|
||||
totalCost?: number;
|
||||
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
|
||||
return {
|
||||
@@ -184,7 +210,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 105 })],
|
||||
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
|
||||
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 5 })],
|
||||
totalCost: 0,
|
||||
})
|
||||
);
|
||||
@@ -207,7 +233,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 110 })],
|
||||
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 10 }],
|
||||
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 10 })],
|
||||
totalCost: 1500,
|
||||
})
|
||||
);
|
||||
@@ -242,6 +268,32 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('counts only per-worker build overage rows, ignoring rows without build metadata', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 103 })],
|
||||
overageMetrics: [
|
||||
createMockBuildOverage({ id: 'real', value: 3 }),
|
||||
// A rollup/summary row with no build metadata - must not be summed in.
|
||||
{ __typename: 'EstimatedOverageAndCost', id: 'rollup', value: 50 },
|
||||
],
|
||||
totalCost: 450,
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"You've used 3 builds beyond your included credits this billing period ($4.50 in overages so far)."
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not warn a Free plan below its limit even if the usage API reports an overage', async () => {
|
||||
// Regression: free users with only a handful of builds were told they'd reached
|
||||
// their limit because a nonzero overage row was treated as billable usage.
|
||||
@@ -249,7 +301,7 @@ describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Free',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 6, limit: 30 })],
|
||||
overageMetrics: [{ __typename: 'EstimatedOverageAndCost', id: 'o1', value: 5 }],
|
||||
overageMetrics: [createMockBuildOverage({ id: 'o1', value: 5 })],
|
||||
totalCost: 0,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -33,7 +33,12 @@ export async function maybeWarnAboutUsageOveragesAsync({
|
||||
return;
|
||||
}
|
||||
|
||||
const overageCount = buildMetrics.overageMetrics.reduce((sum, o) => sum + o.value, 0);
|
||||
// Count only genuine per-worker build overage rows. `overageMetrics` can also
|
||||
// contain rows without build metadata (e.g. rollups), which must not be summed
|
||||
// in — otherwise the displayed "N builds beyond your credits" count is inflated.
|
||||
const overageCount = buildMetrics.overageMetrics
|
||||
.filter(o => o.metadata?.billingResourceClass && o.metadata?.platform)
|
||||
.reduce((sum, o) => sum + o.value, 0);
|
||||
const overageCostCents = buildMetrics.totalCost;
|
||||
const hasFreePlan = subscription.name === 'Free';
|
||||
const tier = classifyUsageTier({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"version": "21.0.1",
|
||||
"version": "21.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": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,8 +26,8 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.1",
|
||||
"@expo/build-tools": "21.0.2",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"@expo/logger": "21.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/turtle-spawn": "21.0.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"env-paths": "^2.2.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"joi": "^17.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.18.1",
|
||||
"nullthrows": "^1.1.1",
|
||||
"semver": "^7.6.2",
|
||||
"tar": "7.5.19",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -27,7 +27,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"@expo/logger": "21.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"arg": "^5.0.2",
|
||||
|
||||
@@ -32,9 +32,6 @@ import {
|
||||
export class StepsConfigParser extends AbstractConfigParser {
|
||||
private readonly steps: Step[];
|
||||
private readonly hooks: Hooks;
|
||||
// Anchors present in the job's own steps, collected during construction.
|
||||
// Used to warn about payload hook keys whose anchor never occurred.
|
||||
private readonly encounteredHookAnchors = new Set<HookAnchorId>();
|
||||
|
||||
constructor(
|
||||
ctx: BuildStepGlobalContext,
|
||||
@@ -105,7 +102,6 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
if (anchorId === undefined) {
|
||||
continue;
|
||||
}
|
||||
this.encounteredHookAnchors.add(anchorId);
|
||||
const anchorHooks = this.constructAnchorHooks(anchorId, validatedHooks, {
|
||||
buildFunctionById,
|
||||
buildFunctionGroupById,
|
||||
@@ -122,7 +118,6 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
buildSteps.push(this.createBuildStepFromNonGroupStepConfig(stepConfig, buildFunctionById));
|
||||
continue;
|
||||
}
|
||||
this.encounteredHookAnchors.add(anchorId);
|
||||
const maps = { buildFunctionById, buildFunctionGroupById };
|
||||
const before = this.constructHookSideEntries(anchorId, 'before', validatedHooks, maps);
|
||||
const anchorStep = this.createBuildStepFromNonGroupStepConfig(stepConfig, buildFunctionById);
|
||||
@@ -133,9 +128,6 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
}
|
||||
}
|
||||
|
||||
// After construction: group expansions also record encountered anchors.
|
||||
this.warnAboutUnmatchedHookKeys();
|
||||
|
||||
return {
|
||||
buildSteps,
|
||||
buildFunctionById,
|
||||
@@ -146,10 +138,9 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
private validateHooks(): Record<string, Step[]> {
|
||||
const validatedHooks: Record<string, Step[]> = {};
|
||||
for (const [hookKey, hookSteps] of Object.entries(this.hooks)) {
|
||||
// Keys that don't name a registered anchor are fully inert — never
|
||||
// validated, never constructed, reported by warnAboutUnmatchedHookKeys.
|
||||
// A worker must not fail on hook keys newer than itself, even when their
|
||||
// steps reference functions it doesn't have yet.
|
||||
// A worker must not fail on a hook key newer than itself, so unregistered
|
||||
// keys skip validation entirely (their steps may reference functions this
|
||||
// worker lacks).
|
||||
if (parseHookKey(hookKey) === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -223,23 +214,6 @@ export class StepsConfigParser extends AbstractConfigParser {
|
||||
return constructHookEntriesFromValidatedSteps(this.ctx, hookSteps, maps);
|
||||
}
|
||||
|
||||
private warnAboutUnmatchedHookKeys(): void {
|
||||
for (const hookKey of Object.keys(this.hooks)) {
|
||||
const parsedHookKey = parseHookKey(hookKey);
|
||||
if (parsedHookKey === null) {
|
||||
this.ctx.baseLogger.warn(
|
||||
{ hookKey },
|
||||
`Unknown hook key "${hookKey}" in the job payload; its steps did not run.`
|
||||
);
|
||||
} else if (!this.encounteredHookAnchors.has(parsedHookKey.anchorId)) {
|
||||
this.ctx.baseLogger.warn(
|
||||
{ hookKey, hookAnchor: parsedHookKey.anchorId },
|
||||
`Hook key "${hookKey}" did not match any step in this job (anchor "${parsedHookKey.anchorId}" never occurred); its steps did not run.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createBuildStepFromNonGroupStepConfig(
|
||||
stepConfig: Step,
|
||||
buildFunctionById: BuildFunctionById
|
||||
|
||||
@@ -468,7 +468,7 @@ describe('StepsConfigParser hooks with function groups', () => {
|
||||
externalFunctionGroups: [createGroup()],
|
||||
});
|
||||
// The group expands normally; the stamp never matches an anchor, so the
|
||||
// submit hook stays unmatched (warned, not run).
|
||||
// submit hook stays unmatched (ignored, not run).
|
||||
expect(workflow.buildSteps).toHaveLength(2);
|
||||
expect(workflow.hooksByAnchorStep.size).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/template-file",
|
||||
"version": "21.0.0",
|
||||
"version": "21.0.2",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "21.0.1",
|
||||
"version": "21.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,17 +19,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "21.0.1",
|
||||
"@expo/build-tools": "21.0.2",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "21.0.0",
|
||||
"@expo/eas-build-job": "21.0.1",
|
||||
"@expo/eas-build-job": "21.0.2",
|
||||
"@expo/logger": "21.0.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "21.0.1",
|
||||
"@expo/template-file": "21.0.0",
|
||||
"@expo/steps": "21.0.2",
|
||||
"@expo/template-file": "21.0.2",
|
||||
"@expo/turtle-spawn": "21.0.0",
|
||||
"@hapi/boom": "10.0.1",
|
||||
"check-disk-space": "3.4.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"koa": "3.1.2",
|
||||
"koa-body": "7.0.1",
|
||||
"koa-router": "14.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.18.1",
|
||||
"micromatch": "4.0.8",
|
||||
"node-fetch": "2.7.0",
|
||||
"node-os-utils": "1.3.7",
|
||||
@@ -48,7 +48,7 @@
|
||||
"tar": "7.5.19",
|
||||
"uuid": "3.4.0",
|
||||
"uuidv7": "1.1.0",
|
||||
"ws": "8.18.0",
|
||||
"ws": "8.21.1",
|
||||
"xml2js": "0.6.2",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RuntimeSettings } from '@expo/build-tools';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import os from 'os';
|
||||
import v8 from 'v8';
|
||||
|
||||
import config from '../config';
|
||||
import { getBuildEnv, getGradleMemoryOptions } from '../env';
|
||||
import { getBuildEnv, getGradleMemoryOptions, getNodeMemoryOptions } from '../env';
|
||||
|
||||
describe(getBuildEnv.name, () => {
|
||||
const originalSentryDsn = config.sentry.dsn;
|
||||
@@ -19,6 +20,7 @@ describe(getBuildEnv.name, () => {
|
||||
EAS_BUILD_MAVEN_CACHE_URL: process.env.EAS_BUILD_MAVEN_CACHE_URL,
|
||||
EAS_BUILD_COCOAPODS_CACHE_URL: process.env.EAS_BUILD_COCOAPODS_CACHE_URL,
|
||||
NPM_CONFIG_REGISTRY: process.env.NPM_CONFIG_REGISTRY,
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -28,6 +30,7 @@ describe(getBuildEnv.name, () => {
|
||||
delete process.env.EAS_BUILD_MAVEN_CACHE_URL;
|
||||
delete process.env.EAS_BUILD_COCOAPODS_CACHE_URL;
|
||||
delete process.env.NPM_CONFIG_REGISTRY;
|
||||
delete process.env.NODE_OPTIONS;
|
||||
jest.spyOn(RuntimeSettings, 'getNpmCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getNodeJsCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getMavenCacheUrl').mockReturnValue(null);
|
||||
@@ -47,6 +50,7 @@ describe(getBuildEnv.name, () => {
|
||||
restoreEnv('EAS_BUILD_MAVEN_CACHE_URL', originalCacheUrls.EAS_BUILD_MAVEN_CACHE_URL);
|
||||
restoreEnv('EAS_BUILD_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_BUILD_COCOAPODS_CACHE_URL);
|
||||
restoreEnv('NPM_CONFIG_REGISTRY', originalCacheUrls.NPM_CONFIG_REGISTRY);
|
||||
restoreEnv('NODE_OPTIONS', originalCacheUrls.NODE_OPTIONS);
|
||||
mockProcessPlatform(originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
@@ -299,6 +303,63 @@ describe(getBuildEnv.name, () => {
|
||||
maxHeapSize: '1g',
|
||||
});
|
||||
});
|
||||
|
||||
it('increases the Node.js heap limit on workers with enough memory', () => {
|
||||
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
|
||||
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
|
||||
heap_size_limit: 2 * 1024 ** 3,
|
||||
} as ReturnType<typeof v8.getHeapStatistics>);
|
||||
|
||||
expect(getNodeMemoryOptions()).toBe('--max-old-space-size=3072');
|
||||
});
|
||||
|
||||
it('does not increase the Node.js heap limit on workers with less than 6 GiB of memory', () => {
|
||||
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3 - 1);
|
||||
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
|
||||
heap_size_limit: 2 * 1024 ** 3,
|
||||
} as ReturnType<typeof v8.getHeapStatistics>);
|
||||
|
||||
expect(getNodeMemoryOptions()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not increase an existing Node.js heap limit of at least 3 GiB', () => {
|
||||
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
|
||||
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
|
||||
heap_size_limit: 3 * 1024 ** 3,
|
||||
} as ReturnType<typeof v8.getHeapStatistics>);
|
||||
|
||||
expect(getNodeMemoryOptions()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps Node.js options inherited from the worker environment in override position', () => {
|
||||
process.env.NODE_OPTIONS = '--max-old-space-size=2048 --enable-source-maps';
|
||||
jest.spyOn(process, 'constrainedMemory').mockReturnValue(6 * 1024 ** 3);
|
||||
jest.spyOn(v8, 'getHeapStatistics').mockReturnValue({
|
||||
heap_size_limit: 2 * 1024 ** 3,
|
||||
} as ReturnType<typeof v8.getHeapStatistics>);
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.ANDROID,
|
||||
type: Workflow.MANAGED,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.NODE_OPTIONS).toBe(
|
||||
'--max-old-space-size=3072 --max-old-space-size=2048 --enable-source-maps'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function mockProcessPlatform(platform: NodeJS.Platform): void {
|
||||
|
||||
@@ -4,12 +4,15 @@ import { spawnSync } from 'child_process';
|
||||
import micromatch from 'micromatch';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import v8 from 'v8';
|
||||
|
||||
import config from './config';
|
||||
import { Environment } from './constants';
|
||||
import { androidImagesWithJavaVersionLowerThen11 } from './external/turtle';
|
||||
import { getAccessedEnvs } from './utils/env';
|
||||
|
||||
const GIB_IN_BYTES = 1024 ** 3;
|
||||
|
||||
// keep in sync with local-build-plugin env vars
|
||||
// see packages/local-build-plugin/src/build.ts
|
||||
export function getBuildEnv({
|
||||
@@ -50,6 +53,9 @@ export function getBuildEnv({
|
||||
setEnv(env, 'EAS_BUILD_WORKINGDIR', path.join(config.workingdir, 'build'));
|
||||
setEnv(env, 'EAS_BUILD_PROJECT_ID', projectId);
|
||||
|
||||
// Keep inherited options last so they override singleton options supplied by the worker.
|
||||
setEnv(env, 'NODE_OPTIONS', [getNodeMemoryOptions(), env.NODE_OPTIONS].filter(Boolean).join(' '));
|
||||
|
||||
const runnerPlatform = job.platform;
|
||||
if (runnerPlatform === Platform.IOS) {
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', cocoapodsCacheUrl);
|
||||
@@ -179,3 +185,17 @@ export function getGradleMemoryOptions() {
|
||||
maxHeapSize: `${Math.max(1, Math.round(totalMemoryGb / 4))}g`,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNodeMemoryOptions(): string | undefined {
|
||||
// constrainedMemory() returns 0 when there is no container or process memory limit.
|
||||
const memoryLimit = process.constrainedMemory() || os.totalmem();
|
||||
const heapLimit = v8.getHeapStatistics().heap_size_limit;
|
||||
|
||||
// V8 defaults to a roughly 2 GiB heap on smaller workers. Raise the ceiling for build child
|
||||
// processes only when the worker has enough memory and Node has not already selected a larger one.
|
||||
if (memoryLimit < 6 * GIB_IN_BYTES || heapLimit >= 3 * GIB_IN_BYTES) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return '--max-old-space-size=3072';
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"chalk": "5.2.0",
|
||||
"dateformat": "4.5.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"lodash-es": "4.18.1",
|
||||
"marked": "3.0.4",
|
||||
"semver": "7.5.4",
|
||||
"tslib": "2.6.2"
|
||||
|
||||
Generated
+5
-5
@@ -213,10 +213,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash-es@npm:4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash-es@npm:4.17.21"
|
||||
checksum: 10c0/fb407355f7e6cd523a9383e76e6b455321f0f153a6c9625e21a8827d10c54c2a2341bd2ae8d034358b60e07325e1330c14c224ff582d04612a46a4f0479ff2f2
|
||||
"lodash-es@npm:4.18.1":
|
||||
version: 4.18.1
|
||||
resolution: "lodash-es@npm:4.18.1"
|
||||
checksum: 10c0/35d4dcf87ef07f8d090f409447575800108057e360b445f590d0d25d09e3d1e33a163d2fc100d4d072b0f901d5e2fc533cd7c4bfd8eeb38a06abec693823c8b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -257,7 +257,7 @@ __metadata:
|
||||
chalk: "npm:5.2.0"
|
||||
dateformat: "npm:4.5.1"
|
||||
fs-extra: "npm:11.2.0"
|
||||
lodash-es: "npm:4.17.21"
|
||||
lodash-es: "npm:4.18.1"
|
||||
marked: "npm:3.0.4"
|
||||
semver: "npm:7.5.4"
|
||||
ts-node: "npm:10.9.2"
|
||||
|
||||
Reference in new issue
Block a user