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 --with-eas-environment-variables-set flag to eas update command #2628

Merged
40 changes: 38 additions & 2 deletions packages/eas-cli/src/commandUtils/EasCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField';
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import { ServerSideEnvironmentVariablesContextField } from './context/ServerSideEnvironmentVariablesContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import VcsClientContextField from './context/VcsClientContextField';
import { EasCommandError } from './errors';
Expand All @@ -23,6 +24,7 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
Expand All @@ -43,8 +45,26 @@ export type ContextOutput<
[P in keyof T]: T[P];
};

szdziedzic marked this conversation as resolved.
Show resolved Hide resolved
type GetContextType<Type> = {
[Property in keyof Type]: any;
};

const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';

interface BaseGetContextAsyncArgs {
nonInteractive: boolean;
vcsClientOverride?: Client;
}

interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
extends BaseGetContextAsyncArgs {
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
}

interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
withServerSideEnvironment?: never;
}

export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
Expand Down Expand Up @@ -72,7 +92,7 @@ export default abstract class EasCommand extends Command {
* run within a project directory, null otherwise.
*/
OptionalProjectConfig: {
privateProjectConfig: new OptionalPrivateProjectConfigContextField(),
optionalPrivateProjectConfig: new OptionalPrivateProjectConfigContextField(),
},
/**
* Require this command to be run in a project directory. Return the project directory in the context.
Expand Down Expand Up @@ -107,6 +127,10 @@ export default abstract class EasCommand extends Command {
Vcs: {
vcsClient: new VcsClientContextField(),
},
ServerSideEnvironmentVariables: {
// eslint-disable-next-line async-protect/async-suffix
getServerSideEnvironmentVariablesAsync: new ServerSideEnvironmentVariablesContextField(),
},
};

/**
Expand Down Expand Up @@ -141,7 +165,18 @@ export default abstract class EasCommand extends Command {
} = object,
>(
commandClass: { contextDefinition: ContextInput<C> },
{ nonInteractive, vcsClientOverride }: { nonInteractive: boolean; vcsClientOverride?: Client }
{
nonInteractive,
vcsClientOverride,
// if specified and not null, the env vars from the selected environment will be fetched from the server
// to resolve dynamic config (if dynamic config context is used) and enable getServerSideEnvironmentVariablesAsync function (if server side environment variables context is used)
withServerSideEnvironment,
}: C extends
| GetContextType<typeof EasCommand.ContextOptions.DynamicProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.OptionalProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.ServerSideEnvironmentVariables>
? GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
: GetContextAsyncArgsWithoutServerSideEnvironmentArgument
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;

Expand All @@ -155,6 +190,7 @@ export default abstract class EasCommand extends Command {
sessionManager: this.sessionManager,
analytics: this.analytics,
vcsClientOverride,
withServerSideEnvironment,
}),
]);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/eas-cli/src/commandUtils/context/ContextField.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';

Expand All @@ -7,6 +8,10 @@ export interface ContextOptions {
analytics: Analytics;
nonInteractive: boolean;
vcsClientOverride?: Client;
/**
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
*/
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
}

export default abstract class ContextField<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ExpoConfig } from '@expo/config';

import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import {
ExpoConfigOptions,
getPrivateExpoConfig,
Expand All @@ -19,6 +21,7 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
Expand All @@ -27,6 +30,24 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
nonInteractive,
env: options?.env,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = getPublicExpoConfig(projectDir, options);
return {
exp,
Expand All @@ -41,6 +62,7 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
Expand All @@ -49,6 +71,24 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
nonInteractive,
env: options?.env,
});
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = getPrivateExpoConfig(projectDir, options);
return {
exp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { ExpoConfig } from '@expo/config';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';

import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPrivateExpoConfig } from '../../project/expoConfig';

export class OptionalPrivateProjectConfigContextField extends ContextField<
Expand All @@ -14,7 +16,11 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
}
| undefined
> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<
| {
projectId: string;
exp: ExpoConfig;
Expand All @@ -39,7 +45,20 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
const exp = getPrivateExpoConfig(projectDir);
let serverSideEnvVars: Record<string, string> | undefined;
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = getPrivateExpoConfig(projectDir, { env: serverSideEnvVars });
return {
exp,
projectDir,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfig } from '../../project/expoConfig';

type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
) => Promise<Record<string, string>>;

export class ServerSideEnvironmentVariablesContextField extends ContextField<GetServerSideEnvironmentVariablesFn> {
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<GetServerSideEnvironmentVariablesFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (maybeEnv?: Record<string, string>) => {
if (!withServerSideEnvironment) {
throw new Error(
'withServerSideEnvironment parameter is required to evaluate ServerSideEnvironmentVariablesContextField'
);
}
const exp = getPublicExpoConfig(projectDir, { env: maybeEnv });
const projectId = await getProjectIdAsync(sessionManager, exp, {
nonInteractive,
env: maybeEnv,
});
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
return serverSideEnvironmentVariables;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ExpoGraphqlClient } from './createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';

const cachedServerSideEnvironmentVariables: Record<
EnvironmentVariableEnvironment,
Record<string, string> | null
> = {
[EnvironmentVariableEnvironment.Development]: null,
[EnvironmentVariableEnvironment.Preview]: null,
[EnvironmentVariableEnvironment.Production]: null,
};

export async function loadServerSideEnvironmentVariablesAsync({
environment,
projectId,
graphqlClient,
}: {
environment: EnvironmentVariableEnvironment;
projectId: string;
graphqlClient: ExpoGraphqlClient;
}): Promise<Record<string, string>> {
// don't load environment variables if they were already loaded while executing a command
const cachedEnvVarsForEnvironment = cachedServerSideEnvironmentVariables[environment];
if (cachedEnvVarsForEnvironment) {
return cachedEnvVarsForEnvironment;
}

const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
const serverEnvVars = Object.fromEntries(
environmentVariables
.filter(({ name, value }) => name && value)
.map(({ name, value }) => [name, value])
) as Record<string, string>;

if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables loaded from the "${environment.toLowerCase()}" environment on EAS servers: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables found for the "${environment.toLowerCase()}" environment on EAS servers.`
);
}

const encryptedEnvVars = environmentVariables.filter(({ name, value }) => name && !value);
if (encryptedEnvVars.length > 0) {
Log.warn(
`Some environment variables defined in the "${environment.toLowerCase()}" environment on EAS servers are of "encrypted" type and cannot be read outside of the EAS servers (including EAS CLI): ${encryptedEnvVars
.map(({ name }) => name)
.join(', ')}. `
);
}
Log.newLine();

cachedServerSideEnvironmentVariables[environment] = serverEnvVars;

return serverEnvVars;
}
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export default class Build extends EasCommand {
vcsClient,
} = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
withServerSideEnvironment: null,
});

await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default class BuildInspect extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
withServerSideEnvironment: null,
});

const outputDirectory = path.resolve(process.cwd(), flags.output);
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default class BuildInternal extends EasCommand {
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
vcsClientOverride: new GitNoCommitClient(),
withServerSideEnvironment: null,
});

await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/resign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class BuildResign extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
withServerSideEnvironment: null,
});

const maybeBuild = flags.maybeBuildId
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/version/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default class BuildVersionGetView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionGetView, {
nonInteractive: true,
withServerSideEnvironment: null,
});

if (!flags.platform && flags['non-interactive']) {
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/version/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionSetView, {
nonInteractive: false,
withServerSideEnvironment: null,
});

const platform = await selectPlatformAsync(flags.platform);
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/version/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default class BuildVersionSyncView extends EasCommand {
vcsClient,
} = await this.getContextAsync(BuildVersionSyncView, {
nonInteractive: true,
withServerSideEnvironment: null,
});

const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
Expand Down
Loading
Loading