Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[eas-cli] Add metadata root actions and error handling #1135

Merged
merged 2 commits into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}