Skip to content

Commit

Permalink
Add eas env:push and env:pull commands (#2495)
Browse files Browse the repository at this point in the history
* Implement `eas env:pull` and `eas env:push` commands

* Implement review suggestions

* Implement review suggestions

* Allow fetching sensitive variables with env:get

* Implement review suggestions

* fix push

* Implement `including-sensitive` for env:list

* Use envvariables in local build

* Remove withSudo

* Remove sudo

* Evaluate dynamicProjectConfig with EnvVars
  • Loading branch information
khamilowicz authored Aug 26, 2024
1 parent 1540b13 commit 56510f0
Show file tree
Hide file tree
Showing 30 changed files with 7,557 additions and 1,040 deletions.
549 changes: 514 additions & 35 deletions packages/eas-cli/graphql.schema.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/eas-cli/src/build/android/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
env: ctx.env,
});
},
prepareJobAsync: async (
Expand Down
4 changes: 2 additions & 2 deletions packages/eas-cli/src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export async function prepareBuildRequestForPlatformAsync<

return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env);
return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
Expand Down Expand Up @@ -676,7 +676,7 @@ async function createAndMaybeUploadFingerprintAsync<T extends Platform>(
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.buildProfile.env,
env: ctx.env,
cwd: ctx.projectDir,
});

Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/build/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ export interface BuildContext<T extends Platform> {
vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
env: Record<string, string>;
}
9 changes: 7 additions & 2 deletions packages/eas-cli/src/build/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel,
freezeCredentials,
repack,
env,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
Expand All @@ -64,8 +65,11 @@ export async function createBuildContextAsync<T extends Platform>({
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env,
});
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
Expand All @@ -87,7 +91,7 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
Expand Down Expand Up @@ -147,6 +151,7 @@ export async function createBuildContextAsync<T extends Platform>({
requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
env,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
Expand Down
81 changes: 81 additions & 0 deletions packages/eas-cli/src/build/evaluateConfigWithEnvVarsAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';

import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log from '../log';

function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env.toUpperCase() as EnvironmentVariableEnvironment
);
}

export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
flags,
buildProfile,
graphqlClient,
getProjectConfig,
opts,
}: {
flags: { environment?: string };
buildProfile: BuildProfile;
graphqlClient: ExpoGraphqlClient | null;
opts: Opts;
getProjectConfig(opts: Opts): Promise<Config>;
}): Promise<Config & { env: Env }> {
if (!graphqlClient) {
Log.warn('An Expo user account is required to fetch environment variables.');
const config = await getProjectConfig(opts);
return { env: buildProfile.env ?? {}, ...config };
}
const { projectId } = await getProjectConfig({ env: buildProfile.env, ...opts });
const env = await resolveEnvVarsAsync({ flags, buildProfile, graphqlClient, projectId });
const config = await getProjectConfig({ ...opts, env });

return { env, ...config };
}

async function resolveEnvVarsAsync({
flags,
buildProfile,
graphqlClient,
projectId,
}: {
flags: { environment?: string };
buildProfile: BuildProfile;
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<Env> {
const environment =
flags.environment ?? buildProfile.environment ?? process.env.EAS_CURRENT_ENVIRONMENT;

if (!environment || !isEnvironment(environment)) {
return { ...buildProfile.env };
}

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

return { ...envVars, ...buildProfile.env };
} catch (e) {
Log.error('Failed to pull env variables for environment ${environment} from EAS servers');
Log.error(e);
Log.error(
'This can possibly be a bug in EAS/EAS CLI. Report it here: https://github.com/expo/eas-cli/issues'
);
return { ...buildProfile.env };
}
}
6 changes: 3 additions & 3 deletions packages/eas-cli/src/build/ios/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile } = ctx;
const { buildProfile, env } = ctx;

if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
Expand All @@ -47,7 +47,7 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
env,
vcsClient: ctx.vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
Expand Down Expand Up @@ -89,7 +89,7 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.buildProfile.env,
env: ctx.env,
});
},
prepareJobAsync: async (
Expand Down
4 changes: 3 additions & 1 deletion packages/eas-cli/src/build/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export interface LocalBuildOptions {
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions
options: LocalBuildOptions,
env: Record<string, string>
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
Expand All @@ -57,6 +58,7 @@ export async function runLocalBuildAsync(
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: {
...env,
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
Expand Down
35 changes: 23 additions & 12 deletions packages/eas-cli/src/build/runBuildAndSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
Expand All @@ -19,6 +18,7 @@ import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './
import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext';
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
Expand All @@ -33,6 +33,7 @@ import {
BuildFragment,
BuildStatus,
BuildWithSubmissionsFragment,
EnvironmentVariableEnvironment,
SubmissionFragment,
} from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
Expand Down Expand Up @@ -96,6 +97,7 @@ export interface BuildFlags {
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
environment?: EnvironmentVariableEnvironment;
}

export async function runBuildAndSubmitAsync(
Expand Down Expand Up @@ -129,13 +131,6 @@ export async function runBuildAndSubmitAsync(
profileName: flags.profile ?? undefined,
projectDir,
});
Log.log(
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
buildProfiles[0].profile.env
? Object.keys(buildProfiles[0].profile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);

for (const buildProfile of buildProfiles) {
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
Expand Down Expand Up @@ -192,6 +187,21 @@ export async function runBuildAndSubmitAsync(

for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);

const { env } = await evaluateConfigWithEnvVarsAsync({
flags,
buildProfile: buildProfile.profile,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
});

Log.log(
`Loaded "env" configuration for the "${buildProfile.profileName}" profile: ${
env ? Object.keys(env).join(', ') : 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);

const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
Expand All @@ -204,6 +214,7 @@ export async function runBuildAndSubmitAsync(
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
Expand Down Expand Up @@ -251,7 +262,6 @@ export async function runBuildAndSubmitAsync(
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
selectedSubmitProfileName: flags.submitProfile,
Expand Down Expand Up @@ -340,6 +350,7 @@ async function prepareAndStartBuildAsync({
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
}: {
projectDir: string;
flags: BuildFlags;
Expand All @@ -352,6 +363,7 @@ async function prepareAndStartBuildAsync({
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
Expand All @@ -374,6 +386,7 @@ async function prepareAndStartBuildAsync({
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
env,
});

if (moreBuilds) {
Expand Down Expand Up @@ -448,7 +461,6 @@ async function prepareAndStartSubmissionAsync({
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
selectedSubmitProfileName,
nonInteractive,
Expand All @@ -457,7 +469,6 @@ async function prepareAndStartSubmissionAsync({
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
selectedSubmitProfileName?: string;
nonInteractive: boolean;
Expand All @@ -469,7 +480,7 @@ async function prepareAndStartSubmissionAsync({
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildProfile.env,
env: buildCtx.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
Expand Down
4 changes: 2 additions & 2 deletions packages/eas-cli/src/build/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';

export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
if (ctx.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
'You set NODE_ENV=production in the build profile or environment variables. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
);
Log.newLine();
}
Expand Down
15 changes: 15 additions & 0 deletions packages/eas-cli/src/commandUtils/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ export const EASEnvironmentFlag = {
}),
};

// NOTE: Used in build commands, should be replaced with EASEnvironmentFlag when
// the feature is public
export const EASEnvironmentFlagHidden = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
description: "Environment variable's environment",
parse: upperCaseAsync,
hidden: true,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
}),
};

export const EASVariableFormatFlag = {
format: Flags.enum({
description: 'Output format',
Expand Down
Loading

0 comments on commit 56510f0

Please sign in to comment.