Skip to content

Commit

Permalink
[eas-cli] Add metadata commands (#1136)
Browse files Browse the repository at this point in the history
* [eas-cli] Add metadata commands

* Add myself as codeowner to the metadata

* [eas-cli] Hide the metadata commands for now

* Add metadata changelog entry

* [eas-cli] Update metadata configure command description

* [eas-cli] Drop non-interactive due to limitations in AppStoreApi

* [eas-cli] Reimplement credentials context for metadata

* [eas-cli] Drop non-interactive flag in metadata command

* [eas-cli] Drop args from metadata:configure

Co-authored-by: Wojciech Kozyra <wojciech.kozyra@swmansion.com>

* [eas-cli] Add retroactive code review changes (#1147)

* [eas-cli] Add public access modifiers to classes

* [eas-cli] Add specific types to date precision remover

* [eas-cli] Add try catch around writing json to always unsubscribe

* [eas-cli] Add async return await to uploadAsync log methods

* [eas-cli] Fix date precision type and test

* [eas-cli] Use submission profile bundle id if defined

* [eas-cli] Drop catch from IO try catch block

* [eas-cli] Move unique helper to global utils

* [eas-cli] Prefer nullish coalescing

* [eas-cli] Move metadata commands to pull and push

* [eas-cli] Drop the platform option from the push command

* [eas-cli] Remove alias and reword descriptions

* [eas-cli] Auto-configure eas.json with metadata when missing (#1156)

* [eas-cli] Polish the command output for release (#1159)

* [eas-cli] Finalize metadata logging output

* [eas-cli] Fix logging validation errors when store.config.json is not found

* [eas-cli] Make all localized entites bold

* [eas-cli] Make version string visible in logs

* [eas-cli] Add note about early beta

* [eas-cli] Remove hidden from meta commands

* [eas-cli] Add metadata topic

* [eas-cli] Rename eas metadata to eas metadata:push in logging

* [eas-cli] Remove unused arg flag from push

Co-authored-by: Wojciech Kozyra <wojciech.kozyra@swmansion.com>
  • Loading branch information
byCedric and wkozyra95 authored Jun 14, 2022
1 parent 968fdd2 commit c057eaa
Show file tree
Hide file tree
Showing 20 changed files with 236 additions and 68 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features

- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136])(https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))

### 🐛 Bug fixes

Expand Down
3 changes: 3 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @barthap @dsokal

packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric

packages/eas-json @dsokal @wkozyra95
3 changes: 3 additions & 0 deletions packages/eas-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
"device": {
"description": "manage Apple devices for Internal Distribution"
},
"metadata": {
"description": "manage store configuration"
},
"project": {
"description": "manage project"
},
Expand Down
64 changes: 64 additions & 0 deletions packages/eas-cli/src/commands/metadata/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import path from 'path';

import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';

export default class MetadataPull extends EasCommand {
static description = 'generate the local store configuration from the app stores';

static flags = {
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
};

async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');

const { flags } = await this.parse(MetadataPull);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });

const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
nonInteractive: false,
});

const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
projectDir,
exp,
profileName: flags.profile,
});

try {
const filePath = await downloadMetadataAsync(metadataCtx);
const relativePath = path.relative(process.cwd(), filePath);

Log.addNewLineIfNone();
Log.log(`🎉 Your store configuration is ready.
- Update the ${chalk.bold(relativePath)} file to prepare the app information.
- Run ${chalk.bold('eas submit')} or manually upload a new app version to the app stores.
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store configuration.
- ${learnMore('https://docs.expo.dev/eas-metadata/introduction/')}`);
} catch (error: any) {
handleMetadataError(error);
}
}
}
55 changes: 55 additions & 0 deletions packages/eas-cli/src/commands/metadata/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core';

import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import Log from '../../log';
import { createMetadataContextAsync } from '../../metadata/context';
import { handleMetadataError } from '../../metadata/errors';
import { uploadMetadataAsync } from '../../metadata/upload';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';

export default class MetadataPush extends EasCommand {
static description = 'sync the local store configuration to the app stores';

static flags = {
profile: Flags.string({
description:
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
}),
};

async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.');

const { flags } = await this.parse(MetadataPush);
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
await getProjectIdAsync(exp);
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });

const credentialsCtx = new CredentialsContext({
exp,
projectDir,
user: await ensureLoggedInAsync(),
nonInteractive: false,
});

const metadataCtx = await createMetadataContextAsync({
credentialsCtx,
projectDir,
exp,
profileName: flags.profile,
});

try {
await uploadMetadataAsync(metadataCtx);
Log.addNewLineIfNone();
Log.log(`🎉 Store configuration is synced with the app stores.`);
} catch (error: any) {
handleMetadataError(error);
}
}
}
22 changes: 11 additions & 11 deletions packages/eas-cli/src/metadata/apple/config/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ReleaseType,
} from '@expo/apple-utils';

import { unique } from '../../utils/array';
import uniq from '../../../utils/expodash/uniq';
import { AttributesOf } from '../../utils/asc';
import { removeDatePrecision } from '../../utils/date';
import { AppleMetadata } from '../types';
Expand All @@ -22,18 +22,18 @@ export const DEFAULT_WHATSNEW = 'Bug fixes and improved stability';
* This uses version 0 of the config schema.
*/
export class AppleConfigReader {
constructor(public readonly schema: AppleMetadata) {}
public constructor(public readonly schema: AppleMetadata) {}

getAgeRating(): Partial<AttributesOf<AgeRatingDeclaration>> | null {
public getAgeRating(): Partial<AttributesOf<AgeRatingDeclaration>> | null {
return this.schema.advisory || null;
}

getLocales(): string[] {
public getLocales(): string[] {
// TODO: filter "default" locales, add option to add non-localized info to the config
return unique(Object.keys(this.schema.info || {}));
return uniq(Object.keys(this.schema.info || {}));
}

getInfoLocale(
public getInfoLocale(
locale: string
): PartialExcept<AttributesOf<AppInfoLocalization>, 'locale' | 'name'> | null {
const info = this.schema.info?.[locale];
Expand All @@ -43,15 +43,15 @@ export class AppleConfigReader {

return {
locale,
name: info.title || 'no name provided',
name: info.title ?? 'no name provided',
subtitle: info.subtitle,
privacyChoicesUrl: info.privacyChoicesUrl,
privacyPolicyText: info.privacyPolicyText,
privacyPolicyUrl: info.privacyPolicyUrl,
};
}

getCategories(): CategoryIds | null {
public getCategories(): CategoryIds | null {
if (Array.isArray(this.schema.categories) && this.schema.categories.length > 0) {
return {
primaryCategory: this.schema.categories[0],
Expand All @@ -63,13 +63,13 @@ export class AppleConfigReader {
}

/** Get the `AppStoreVersion` object. */
getVersion(): Partial<
public getVersion(): Partial<
Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
return this.schema.copyright ? { copyright: this.schema.copyright } : null;
}

getVersionRelease(): Partial<
public getVersionRelease(): Partial<
Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
const { release } = this.schema;
Expand Down Expand Up @@ -99,7 +99,7 @@ export class AppleConfigReader {
return null;
}

getVersionLocale(
public getVersionLocale(
locale: string,
context: { versionIsFirst: boolean }
): Partial<AttributesOf<AppStoreVersionLocalization>> | null {
Expand Down
14 changes: 7 additions & 7 deletions packages/eas-cli/src/metadata/apple/config/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ export class AppleConfigWriter {
constructor(public readonly schema: Partial<AppleMetadata> = {}) {}

/** Get the schema result to write it to the config file */
toSchema(): { configVersion: number; apple: Partial<AppleMetadata> } {
public toSchema(): { configVersion: number; apple: Partial<AppleMetadata> } {
return {
configVersion: 0,
apple: this.schema,
};
}

setAgeRating(attributes: AttributesOf<AgeRatingDeclaration>): void {
public setAgeRating(attributes: AttributesOf<AgeRatingDeclaration>): void {
this.schema.advisory = attributes;
}

setInfoLocale(attributes: AttributesOf<AppInfoLocalization>): void {
public setInfoLocale(attributes: AttributesOf<AppInfoLocalization>): void {
this.schema.info = this.schema.info ?? {};
const existing = this.schema.info[attributes.locale] ?? {};

Expand All @@ -43,7 +43,7 @@ export class AppleConfigWriter {
};
}

setCategories({ primaryCategory, secondaryCategory }: AttributesOf<AppInfo>): void {
public setCategories({ primaryCategory, secondaryCategory }: AttributesOf<AppInfo>): void {
this.schema.categories = [];

// TODO: see why these types are conflicting
Expand All @@ -55,13 +55,13 @@ export class AppleConfigWriter {
}
}

setVersion(
public setVersion(
attributes: Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
this.schema.copyright = optional(attributes.copyright);
}

setVersionRelease(
public setVersionRelease(
attributes: Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
if (attributes.releaseType === ReleaseType.SCHEDULED) {
Expand All @@ -83,7 +83,7 @@ export class AppleConfigWriter {
}
}

setVersionLocale(attributes: AttributesOf<AppStoreVersionLocalization>): void {
public setVersionLocale(attributes: AttributesOf<AppStoreVersionLocalization>): void {
this.schema.info = this.schema.info ?? {};
const existing = this.schema.info[attributes.locale] ?? {};

Expand Down
8 changes: 4 additions & 4 deletions packages/eas-cli/src/metadata/apple/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { AppleData, PartialAppleData } from './data';

export abstract class AppleTask {
/** Get a description from the task to use as section headings in the log */
abstract name(): string;
public abstract name(): string;

/** Prepare the data from the App Store to start syncing with the store configuration */
abstract prepareAsync(options: TaskPrepareOptions): Promise<void>;
public abstract prepareAsync(options: TaskPrepareOptions): Promise<void>;

/** Download all information from the App Store to generate the store configuration */
abstract downloadAsync(options: TaskDownloadOptions): Promise<void>;
public abstract downloadAsync(options: TaskDownloadOptions): Promise<void>;

/** Upload all information from the store configuration to the App Store */
abstract uploadAsync(options: TaskUploadOptions): Promise<void>;
public abstract uploadAsync(options: TaskUploadOptions): Promise<void>;
}

export type TaskPrepareOptions = {
Expand Down
8 changes: 4 additions & 4 deletions packages/eas-cli/src/metadata/apple/tasks/age-rating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ export type AgeRatingData = {
};

export class AgeRatingTask extends AppleTask {
name = (): string => 'age rating declarations';
public name = (): string => 'age rating declarations';

async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
assert(context.version, `App version information is not prepared, can't update age rating`);
context.ageRating = (await context.version.getAgeRatingDeclarationAsync()) || undefined;
}

async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
if (context.ageRating) {
config.setAgeRating(context.ageRating.attributes);
}
}

async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
assert(context.ageRating, `Age rating not initialized, can't update age rating`);

const ageRating = config.getAgeRating();
Expand Down
27 changes: 16 additions & 11 deletions packages/eas-cli/src/metadata/apple/tasks/app-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ export type AppInfoData = {
};

export class AppInfoTask extends AppleTask {
name = (): string => 'app information';
public name = (): string => 'app information';

async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
const info = await retryIfNullAsync(() => context.app.getEditAppInfoAsync());
assert(info, 'Could not resolve the editable app info to update');

context.info = info;
context.infoLocales = await info.getLocalizationsAsync();
}

async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
assert(context.info, `App info not initialized, can't download info`);

config.setCategories(context.info.attributes);
Expand All @@ -35,7 +35,7 @@ export class AppInfoTask extends AppleTask {
}
}

async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
assert(context.info, `App info not initialized, can't update info`);

const categories = config.getCategories();
Expand Down Expand Up @@ -68,14 +68,19 @@ export class AppInfoTask extends AppleTask {

const model = context.infoLocales.find(model => model.attributes.locale === locale);
await logAsync(
() =>
model
? model.updateAsync(attributes)
: context.info.createLocalizationAsync({ ...attributes, locale }),
async () => {
return model
? await model.updateAsync(attributes)
: await context.info.createLocalizationAsync({ ...attributes, locale });
},
{
pending: `${model ? 'Updating' : 'Creating'} localized info for ${locale}...`,
success: `${model ? 'Updated' : 'Created'} localized info for ${locale}`,
failure: `Failed ${model ? 'updating' : 'creating'} localized info for ${locale}`,
pending: `${model ? 'Updating' : 'Creating'} localized info for ${chalk.bold(
locale
)}...`,
success: `${model ? 'Updated' : 'Created'} localized info for ${chalk.bold(locale)}`,
failure: `Failed ${model ? 'updating' : 'creating'} localized info for ${chalk.bold(
locale
)}`,
}
);
}
Expand Down
Loading

0 comments on commit c057eaa

Please sign in to comment.