-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[eas-cli] Add metadata root actions and error handling (#1135)
* [eas-cli] Add metadata root actions and error handling * [eas-cli] Update download and upload metadata methods to new ctx
- Loading branch information
Showing
3 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import fs from 'fs-extra'; | ||
import path from 'path'; | ||
|
||
import { MetadataEvent } from '../analytics/events'; | ||
import { confirmAsync } from '../prompts'; | ||
import { AppleData } from './apple/data'; | ||
import { createAppleTasks } from './apple/tasks'; | ||
import { createAppleWriter } from './config'; | ||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context'; | ||
import { MetadataDownloadError, MetadataValidationError } from './errors'; | ||
import { subscribeTelemetry } from './utils/telemetry'; | ||
|
||
/** | ||
* Generate a local store configuration from the stores. | ||
* Note, only App Store is supported at this time. | ||
*/ | ||
export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promise<string> { | ||
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath); | ||
const fileExists = await fs.pathExists(filePath); | ||
|
||
if (fileExists) { | ||
const overwrite = await confirmAsync({ | ||
message: `Do you want to overwrite the existing store configuration "${metadataCtx.metadataPath}"?`, | ||
}); | ||
if (!overwrite) { | ||
throw new MetadataValidationError(`Store configuration already exists at "${filePath}"`); | ||
} | ||
} | ||
|
||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx); | ||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry( | ||
MetadataEvent.APPLE_METADATA_DOWNLOAD, | ||
{ app, auth } | ||
); | ||
|
||
const errors: Error[] = []; | ||
const config = createAppleWriter(); | ||
const tasks = createAppleTasks(metadataCtx); | ||
const taskCtx = { app }; | ||
|
||
for (const task of tasks) { | ||
try { | ||
await task.prepareAsync({ context: taskCtx }); | ||
} catch (error: any) { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
for (const task of tasks) { | ||
try { | ||
await task.downloadAsync({ config, context: taskCtx as AppleData }); | ||
} catch (error: any) { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
await fs.writeJson(filePath, config.toSchema(), { spaces: 2 }); | ||
unsubscribeTelemetry(); | ||
|
||
if (errors.length > 0) { | ||
throw new MetadataDownloadError(errors, executionId); | ||
} | ||
|
||
return filePath; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import type { ErrorObject } from 'ajv'; | ||
|
||
import Log, { link } from '../log'; | ||
|
||
/** | ||
* Before syncing data to the ASC API, we need to validate the metadata config. | ||
* This error represents unrecoverable issues before syncing that data, | ||
* and should contain useful information for the user to solve before trying again. | ||
*/ | ||
export class MetadataValidationError extends Error { | ||
constructor(message?: string, public readonly errors?: ErrorObject[]) { | ||
super(message ?? 'Store configuration validation failed'); | ||
} | ||
} | ||
|
||
/** | ||
* If a single entity failed to update, we don't block the other entities from uploading. | ||
* We still attempt to update the data in the stores as much as possible. | ||
* Because of that, we keep track of any errors encountered and throw this generic error. | ||
* It contains that list of encountered errors to present to the user. | ||
*/ | ||
export class MetadataUploadError extends Error { | ||
constructor(public readonly errors: Error[], public readonly executionId: string) { | ||
super( | ||
`Store configuration upload encountered ${ | ||
errors.length === 1 ? 'an error' : `${errors.length} errors` | ||
}.` | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* If a single entity failed to download, we don't block the other entities from downloading. | ||
* We sill attempt to pull in the data from the stores as much as possible. | ||
* Because of that, we keep track of any errors envountered and throw this generic error. | ||
* It contains that list of encountered errors to present to the user. | ||
*/ | ||
export class MetadataDownloadError extends Error { | ||
constructor(public readonly errors: Error[], public readonly executionId: string) { | ||
super( | ||
`Store configuration download encountered ${ | ||
errors.length === 1 ? 'an error' : `${errors.length} errors` | ||
}.` | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Handle a thrown metadata error by informing the user what went wrong. | ||
* If a normal error is thrown, this method will re-throw that error to avoid consuming it. | ||
*/ | ||
export function handleMetadataError(error: Error): void { | ||
if (error instanceof MetadataValidationError) { | ||
Log.error(error.message); | ||
Log.log(error.errors?.map(err => ` - ${err.dataPath} ${err.message}`).join('\n')); | ||
return; | ||
} | ||
|
||
if (error instanceof MetadataDownloadError || error instanceof MetadataUploadError) { | ||
Log.error(error.message); | ||
Log.log('Please check the logs for any configuration issues.'); | ||
Log.log('If this issue persists, please open a new issue at:'); | ||
// TODO: add execution ID to the issue template link | ||
Log.log(link('https://github.com/expo/eas-cli')); | ||
return; | ||
} | ||
|
||
throw error; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import fs from 'fs-extra'; | ||
import path from 'path'; | ||
|
||
import { MetadataEvent } from '../analytics/events'; | ||
import { AppleData } from './apple/data'; | ||
import { createAppleTasks } from './apple/tasks'; | ||
import { createAppleReader, validateConfig } from './config'; | ||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context'; | ||
import { MetadataUploadError, MetadataValidationError } from './errors'; | ||
import { subscribeTelemetry } from './utils/telemetry'; | ||
|
||
/** | ||
* Sync a local store configuration with the stores. | ||
* Note, only App Store is supported at this time. | ||
*/ | ||
export async function uploadMetadataAsync(metadataCtx: MetadataContext): Promise<void> { | ||
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath); | ||
if (!(await fs.pathExists(filePath))) { | ||
throw new MetadataValidationError(`Store configuration file not found "${filePath}"`); | ||
} | ||
|
||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx); | ||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry( | ||
MetadataEvent.APPLE_METADATA_UPLOAD, | ||
{ app, auth } | ||
); | ||
|
||
const fileData = await fs.readJson(filePath); | ||
const { valid, errors: validationErrors } = validateConfig(fileData); | ||
if (!valid) { | ||
throw new MetadataValidationError(`Store configuration errors found`, validationErrors); | ||
} | ||
|
||
const errors: Error[] = []; | ||
const config = createAppleReader(fileData); | ||
const tasks = createAppleTasks(metadataCtx); | ||
const taskCtx = { app }; | ||
|
||
for (const task of tasks) { | ||
try { | ||
await task.prepareAsync({ context: taskCtx }); | ||
} catch (error: any) { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
for (const task of tasks) { | ||
try { | ||
await task.uploadAsync({ config, context: taskCtx as AppleData }); | ||
} catch (error: any) { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
unsubscribeTelemetry(); | ||
|
||
if (errors.length > 0) { | ||
throw new MetadataUploadError(errors, executionId); | ||
} | ||
} |