Skip to content

Commit

Permalink
[eas-cli] Add metadata root actions and error handling (#1135)
Browse files Browse the repository at this point in the history
* [eas-cli] Add metadata root actions and error handling

* [eas-cli] Update download and upload metadata methods to new ctx
  • Loading branch information
byCedric authored Jun 4, 2022
1 parent 4c6226b commit 940e3f6
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
65 changes: 65 additions & 0 deletions packages/eas-cli/src/metadata/download.ts
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;
}
69 changes: 69 additions & 0 deletions packages/eas-cli/src/metadata/errors.ts
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;
}
60 changes: 60 additions & 0 deletions packages/eas-cli/src/metadata/upload.ts
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);
}
}

0 comments on commit 940e3f6

Please sign in to comment.