Compare commits

...
5 Commits
Author SHA1 Message Date
Expo CI 10ec54345a v16.23.0 2025-10-14 15:53:39 +00:00
Kadi Kraman 27b4710264 [eas-cli] [eas-json] support custom environment variable environments (#3220) 2025-10-14 13:56:42 +01:00
Kadi Kraman 4132e92da5 Add tests for eas env:push --force (#3223) 2025-10-14 11:31:36 +01:00
Giorgio FellipeandKadi Kraman 264340a717 Add --force parameter to env:push (#3135)
Co-authored-by: Kadi Kraman <[email protected]>
2025-10-14 10:40:22 +01:00
Expo CI 1d09c2c97d update CHANGELOG.md 2025-10-13 20:55:14 +00:00
45 changed files with 928 additions and 614 deletions

No files matched your search

+8 -1
View File
@@ -7,13 +7,20 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
### 🎉 New features
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
### 🐛 Bug fixes
### 🧹 Chores
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
### 🎉 New features
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
### 🎉 New features
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"version": "16.22.0",
"version": "16.23.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+206 -205
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.22.0",
"version": "16.23.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -12,8 +12,8 @@
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.231",
"@expo/eas-json": "16.20.1",
"@expo/eas-build-job": "1.0.237",
"@expo/eas-json": "16.23.0",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.221",
@@ -8,7 +8,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -30,7 +29,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
environment: job.environment,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -1,7 +1,7 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment, isEnvironment } from './utils/environment';
import { DefaultEnvironment } from './utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
@@ -48,20 +48,9 @@ async function resolveEnvVarsAsync({
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ??
buildProfile.environment ??
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
if (!isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
? Object.keys(buildProfile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...buildProfile.env };
}
try {
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
@@ -128,16 +117,16 @@ async function resolveEnvVarsAsync({
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
buildProfile: BuildProfile
): EnvironmentVariableEnvironment {
): DefaultEnvironment {
const environment =
buildProfile.distribution === 'store'
? EnvironmentVariableEnvironment.Production
? DefaultEnvironment.Production
: buildProfile.developmentClient
? EnvironmentVariableEnvironment.Development
: EnvironmentVariableEnvironment.Preview;
? DefaultEnvironment.Development
: DefaultEnvironment.Preview;
Log.log(
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
`Resolved "${environment}" environment for the build. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
)}`
);
+1 -2
View File
@@ -9,7 +9,6 @@ import {
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -32,7 +31,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
environment: job.environment,
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
@@ -1,31 +1,5 @@
import { BuildProfile } from '@expo/eas-json';
type Environment = NonNullable<BuildProfile['environment']>;
// Default environments
export enum EnvironmentVariableEnvironment {
export enum DefaultEnvironment {
Development = 'development',
Preview = 'preview',
Production = 'production',
}
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env.toLowerCase() as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
}
@@ -26,7 +26,6 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
import Log, { link } from '../log';
import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
@@ -60,7 +59,7 @@ interface BaseGetContextAsyncArgs {
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
extends BaseGetContextAsyncArgs {
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
withServerSideEnvironment: string | null;
}
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
@@ -1,5 +1,4 @@
import { Analytics } from '../../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';
@@ -11,7 +10,7 @@ export interface ContextOptions {
/**
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
*/
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
withServerSideEnvironment?: string | null;
}
export default abstract class ContextField<T> {
@@ -1,23 +1,15 @@
import { ExpoGraphqlClient } from './createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
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,
};
const cachedServerSideEnvironmentVariables: Record<string, Record<string, string> | undefined> = {};
export async function loadServerSideEnvironmentVariablesAsync({
environment,
projectId,
graphqlClient,
}: {
environment: EnvironmentVariableEnvironment;
environment: string;
projectId: string;
graphqlClient: ExpoGraphqlClient;
}): Promise<Record<string, string>> {
@@ -42,13 +34,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment}" environment on EAS: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment}" environment on EAS.`
);
}
Log.newLine();
+8 -25
View File
@@ -1,17 +1,5 @@
import { Flags } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
// are used in tandem, it saves on unnecessary typying in commands
async function upperCaseAsync<T>(input: string): Promise<T> {
return input.toUpperCase() as T;
}
function mapToLowercase<T extends string>(options: T[]): T[] {
return options.map(option => option.toLowerCase()) as T[];
}
export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
@@ -23,20 +11,17 @@ export const EasNonInteractiveAndJsonFlags = {
};
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
environment: Flags.string({
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
}),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
multiple: true,
}),
@@ -80,12 +65,10 @@ export const EasJsonOnlyFlag = {
};
export const EasUpdateEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
environment: Flags.string({
description:
'Environment to use for the server-side defined EAS environment variables during command execution.',
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
parse: upperCaseAsync,
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
required: false,
default: null,
default: undefined,
}),
};
@@ -10,7 +10,6 @@ import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEn
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
@@ -48,7 +47,7 @@ interface BuildResignFlags {
sourceProfile?: string;
maybeBuildId?: string;
wait: boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
}
interface RawBuildResignFlags {
json: boolean;
@@ -60,7 +59,7 @@ interface RawBuildResignFlags {
'source-profile': string | undefined;
wait: boolean;
id: string | undefined;
environment: EnvironmentVariableEnvironment | undefined;
environment: string | undefined;
}
export default class BuildResign extends EasCommand {
@@ -5,7 +5,6 @@ import chalk from 'chalk';
import fs from 'node:fs';
import * as path from 'node:path';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import { getHostingDeploymentsUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
@@ -45,7 +44,7 @@ interface DeployFlags {
json: boolean;
isProduction: boolean;
aliasName?: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
deploymentIdentifier?: string;
exportDir: string;
dryRun: boolean;
@@ -53,7 +52,7 @@ interface DeployFlags {
interface RawDeployFlags {
'non-interactive': boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
json: boolean;
prod: boolean;
alias?: string;
+12 -15
View File
@@ -1,7 +1,7 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -103,7 +103,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
type: EnvironmentSecretType.String,
},
@@ -140,7 +140,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Project,
},
]);
@@ -168,7 +168,7 @@ describe(EnvCreate, () => {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Secret,
});
});
@@ -201,7 +201,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -236,7 +236,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: 'otherId',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
@@ -274,7 +274,7 @@ describe(EnvCreate, () => {
.mockImplementation(async () => [
{
id: otherVariableId,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Shared,
},
]);
@@ -285,7 +285,7 @@ describe(EnvCreate, () => {
id: otherVariableId,
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
});
@@ -320,10 +320,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [
EnvironmentVariableEnvironment.Production,
EnvironmentVariableEnvironment.Development,
],
environments: [DefaultEnvironment.Production, DefaultEnvironment.Development],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -343,7 +340,7 @@ describe(EnvCreate, () => {
jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
.mockImplementation(async () => [DefaultEnvironment.Production]);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -361,7 +358,7 @@ describe(EnvCreate, () => {
{
name: 'VarName',
value: 'VarValue',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -400,7 +397,7 @@ describe(EnvCreate, () => {
{
name: 'TEST_VAR',
value: 'test-value',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
@@ -1,6 +1,6 @@
import { Config } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import {
EnvironmentSecretType,
EnvironmentVariableScope,
@@ -37,7 +37,7 @@ describe(EnvDelete, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -129,7 +129,7 @@ describe(EnvDelete, () => {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -156,7 +156,7 @@ describe(EnvDelete, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
environment: EnvironmentVariableEnvironment.Development,
environment: DefaultEnvironment.Development,
});
});
});
+13 -7
View File
@@ -1,6 +1,6 @@
import { Config } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -24,7 +24,7 @@ describe(EnvExec, () => {
id: 'var1',
name: 'EXPO_PUBLIC_API_URL',
value: 'https://api.example.com',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -58,13 +58,13 @@ describe(EnvExec, () => {
graphqlClient,
{
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Development,
environment: DefaultEnvironment.Development,
}
);
});
it('rejects invalid environment when using positional argument', async () => {
const command = new EnvExec(['invalid', 'echo test'], mockConfig);
it('accepts custom environment when using positional argument', async () => {
const command = new EnvExec(['custom-environment', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -72,8 +72,14 @@ describe(EnvExec, () => {
projectId: testProjectId,
});
await expect(command.runAsync()).rejects.toThrow(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'custom-environment',
}
);
});
});
+5 -5
View File
@@ -1,6 +1,6 @@
import { Config } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -27,7 +27,7 @@ describe(EnvGet, () => {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -82,7 +82,7 @@ describe(EnvGet, () => {
it('prompts for variable name and environment if the name is ambigous', async () => {
jest
.mocked(promptVariableEnvironmentAsync)
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
.mockResolvedValueOnce(DefaultEnvironment.Production);
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
const command = new EnvGet([], mockConfig);
@@ -98,7 +98,7 @@ describe(EnvGet, () => {
id: 'var2',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -135,7 +135,7 @@ describe(EnvGet, () => {
graphqlClient,
{
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Development,
environment: DefaultEnvironment.Development,
filterNames: ['TEST_VAR_1'],
includeFileContent: true,
}
+23 -29
View File
@@ -1,7 +1,7 @@
import { Config } from '@oclif/core';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -13,10 +13,12 @@ import {
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptVariableEnvironmentAsync } from '../../../utils/prompts';
import EnvList from '../list';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../utils/prompts');
jest.mock('../../../log');
const mockVariables: EnvironmentVariableFragment[] = [
@@ -24,7 +26,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
@@ -35,7 +37,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
id: 'var2',
name: 'TEST_VAR_2',
value: 'value2',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
@@ -51,27 +53,19 @@ describe(EnvList, () => {
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});
it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
const command = new EnvList([], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
async (args: any) => {
if (args.nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (args.multiple) {
return [DefaultEnvironment.Production];
}
return DefaultEnvironment.Production;
}
);
});
it('lists project environment variables in specified environments', async () => {
@@ -88,7 +82,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
@@ -110,7 +104,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Development,
environment: DefaultEnvironment.Development,
includeFileContent: false,
});
});
@@ -133,7 +127,7 @@ describe(EnvList, () => {
graphqlClient,
{
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
}
);
@@ -155,7 +149,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
@@ -178,7 +172,7 @@ describe(EnvList, () => {
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: DefaultEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
+17 -10
View File
@@ -2,7 +2,7 @@ import { Config } from '@oclif/core';
import * as fs from 'fs-extra';
import path from 'path';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -34,7 +34,7 @@ describe(EnvPull, () => {
id: 'var1',
name: 'EXPO_PUBLIC_API_URL',
value: 'https://api.example.com',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -45,7 +45,7 @@ describe(EnvPull, () => {
id: 'var2',
name: 'DATABASE_URL',
value: 'postgres://localhost:5432/mydb',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -56,7 +56,7 @@ describe(EnvPull, () => {
id: 'var3',
name: 'SECRET_KEY',
value: 'super-secret-key',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -67,7 +67,7 @@ describe(EnvPull, () => {
id: 'var4',
name: 'CONFIG_FILE',
value: 'base64-encoded-file-content',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -95,7 +95,7 @@ describe(EnvPull, () => {
});
describe('environment validation', () => {
it('accepts development environment (case insensitive)', async () => {
it('accepts development environment', async () => {
const command = new EnvPull(['development'], mockConfig);
// @ts-expect-error
@@ -117,8 +117,8 @@ describe(EnvPull, () => {
);
});
it('rejects invalid environment', async () => {
const command = new EnvPull(['invalid'], mockConfig);
it('accepts custom environment', async () => {
const command = new EnvPull(['custom-environment'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -127,8 +127,15 @@ describe(EnvPull, () => {
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
graphqlClient,
{
appId: testProjectId,
environment: 'custom-environment',
includeFileContent: true,
}
);
});
});
+209 -20
View File
@@ -1,7 +1,7 @@
import { Config } from '@oclif/core';
import * as fs from 'fs-extra';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
@@ -12,12 +12,14 @@ import {
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import EnvPush from '../push';
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('fs-extra');
jest.mock('../../../log');
jest.mock('../../../prompts');
describe(EnvPush, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
@@ -26,12 +28,14 @@ describe(EnvPush, () => {
const testEnvPath = '.env.test';
const mockEnvContent = `EXPO_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgres://localhost:5432/mydb
VARIABLE_NAME=variable value
SECRET_KEY=super-secret-key`;
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(Log, 'log').mockImplementation(() => {});
jest.spyOn(Log, 'warn').mockImplementation(() => {});
jest.spyOn(Log, 'error').mockImplementation(() => {});
// Mock fs-extra methods
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
@@ -39,17 +43,10 @@ SECRET_KEY=super-secret-key`;
// Mock GraphQL queries and mutations
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([]); // No existing variables
jest.mocked(EnvironmentVariableMutation.createForAppAsync).mockResolvedValue({
id: 'var1',
name: 'EXPO_PUBLIC_API_URL',
value: 'https://api.example.com',
environments: [EnvironmentVariableEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
});
jest
.mocked(EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync)
.mockResolvedValue(true);
jest.mocked(confirmAsync).mockResolvedValue(true);
});
it('accepts development environment when using positional argument', async () => {
@@ -68,13 +65,13 @@ SECRET_KEY=super-secret-key`;
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Development,
filterNames: ['EXPO_PUBLIC_API_URL', 'DATABASE_URL', 'SECRET_KEY'],
environment: DefaultEnvironment.Development,
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
});
});
it('rejects invalid environment when using positional argument', async () => {
const command = new EnvPush(['invalid', '--path', testEnvPath], mockConfig);
it('accepts custom environment', async () => {
const command = new EnvPush(['custom-environment', '--path', testEnvPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
@@ -83,8 +80,200 @@ SECRET_KEY=super-secret-key`;
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
await command.runAsync();
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: 'custom-environment',
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
});
});
describe('--force option', () => {
it('automatically overrides existing variables without confirmation when --force is used', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should not call confirmAsync for existing variables
expect(confirmAsync).not.toHaveBeenCalled();
// Should log that force flag is being used
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
// Should call the mutation with overwrite flag
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).toHaveBeenCalledWith(
graphqlClient,
expect.arrayContaining([
expect.objectContaining({
name: 'VARIABLE_NAME',
value: 'variable value',
environments: [DefaultEnvironment.Development],
overwrite: true,
}),
]),
testProjectId
);
});
it('automatically overrides sensitive variables without confirmation when --force is used', async () => {
const existingSensitiveVariable = {
id: 'var1',
name: 'SECRET_KEY',
value: 'old-secret-key',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
.mockResolvedValue([existingSensitiveVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should not call confirmAsync for sensitive variables
expect(confirmAsync).not.toHaveBeenCalled();
// Should log that force flag is being used for sensitive variables
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
// Should call the mutation with overwrite flag
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).toHaveBeenCalledWith(
graphqlClient,
expect.arrayContaining([
expect.objectContaining({
name: 'SECRET_KEY',
value: 'super-secret-key',
environments: [DefaultEnvironment.Development],
overwrite: true,
}),
]),
testProjectId
);
});
it('logs appropriate messages when using --force flag', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should log both force messages
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
expect(Log.log).toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
});
it('still prompts for confirmation when --force is not used', async () => {
const existingVariable = {
id: 'var1',
name: 'VARIABLE_NAME',
value: 'old variable value',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Sensitive,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
jest.mocked(confirmAsync).mockResolvedValue(true);
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
// Should call confirmAsync for both existing variables and sensitive variables
expect(confirmAsync).toHaveBeenCalledTimes(2);
expect(confirmAsync).toHaveBeenCalledWith({
message:
'The VARIABLE_NAME environment variable already exists in development environment. Do you want to override it?',
});
expect(confirmAsync).toHaveBeenCalledWith({
message:
'You are about to overwrite sensitive variables.\n- VARIABLE_NAME\n Do you want to continue?',
});
// Should not log force messages
expect(Log.log).not.toHaveBeenCalledWith(
'Using --force flag: automatically overriding existing variables.'
);
expect(Log.log).not.toHaveBeenCalledWith(
'Using --force flag: automatically overriding sensitive variables.'
);
});
});
});
@@ -2,7 +2,7 @@ import { Config } from '@oclif/core';
import chalk from 'chalk';
import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
import { DefaultEnvironment } from '../../../build/utils/environment';
import {
EnvironmentSecretType,
EnvironmentVariableScope,
@@ -12,12 +12,18 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
import {
parseVisibility,
promptVariableEnvironmentAsync,
promptVariableVisibilityAsync,
} from '../../../utils/prompts';
import EnvUpdate from '../update';
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
jest.mock('../../../prompts');
jest.mock('../../../utils/prompts');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../log');
@@ -34,6 +40,41 @@ describe(EnvUpdate, () => {
beforeEach(() => {
jest.resetAllMocks();
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
async (args: any) => {
if (args.nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (args.multiple) {
return ['production'];
}
return 'production';
}
);
(jest.mocked(promptVariableVisibilityAsync) as jest.MockedFunction<any>).mockResolvedValue(
EnvironmentVariableVisibility.Public
);
(jest.mocked(parseVisibility) as jest.MockedFunction<any>).mockImplementation(
(visibility: string) => {
switch (visibility) {
case 'plaintext':
return EnvironmentVariableVisibility.Public;
case 'sensitive':
return EnvironmentVariableVisibility.Sensitive;
case 'secret':
return EnvironmentVariableVisibility.Secret;
default:
throw new Error(`Invalid visibility: ${visibility}`);
}
}
);
(jest.mocked(selectAsync) as jest.MockedFunction<any>).mockResolvedValue({
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
});
});
it('updates a project variable by selected name in non-interactive mode', async () => {
@@ -42,7 +83,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Project,
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
},
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -74,7 +115,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
visibility: EnvironmentVariableVisibility.Public,
});
expect(Log.withTick).toHaveBeenCalledWith(
@@ -88,7 +129,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'TEST_VARIABLE',
scope: EnvironmentVariableScope.Shared,
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
},
];
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
@@ -120,7 +161,7 @@ describe(EnvUpdate, () => {
id: variableId,
name: 'NEW_VARIABLE',
value: 'new-value',
environments: [EnvironmentVariableEnvironment.Production],
environments: [DefaultEnvironment.Production],
});
expect(Log.withTick).toHaveBeenCalledWith(
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
@@ -130,6 +171,7 @@ describe(EnvUpdate, () => {
it('prompts for variable selection when selected name is not provided', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
{ id: '2', name: 'ANOTHER_VARIABLE', scope: EnvironmentVariableScope.Project },
];
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
@@ -141,7 +183,7 @@ describe(EnvUpdate, () => {
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await command.runAsync();
expect(promptAsync).toHaveBeenCalled();
expect(selectAsync).toHaveBeenCalled();
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ id: variableId })
@@ -169,7 +211,7 @@ describe(EnvUpdate, () => {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [EnvironmentVariableEnvironment.Development],
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
@@ -205,7 +247,7 @@ describe(EnvUpdate, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: projectId,
environment: EnvironmentVariableEnvironment.Development,
environment: DefaultEnvironment.Development,
filterNames: ['TEST_VAR_1'],
});
});
+22 -24
View File
@@ -3,8 +3,8 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
@@ -33,7 +33,6 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
interface RawCreateFlags {
name?: string;
@@ -42,7 +41,7 @@ interface RawCreateFlags {
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
'non-interactive': boolean;
}
@@ -53,7 +52,7 @@ interface CreateFlags {
type?: 'string' | 'file';
visibility?: 'plaintext' | 'sensitive' | 'secret';
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
'non-interactive': boolean;
}
@@ -64,7 +63,7 @@ export default class EnvCreate extends EasCommand {
{
name: 'environment',
description:
"Environment to create the variable in. One of 'production', 'preview', or 'development'.",
"Environment to create the variable in. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -101,6 +100,13 @@ export default class EnvCreate extends EasCommand {
const validatedFlags = this.sanitizeFlags(flags);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvCreate, {
nonInteractive: validatedFlags['non-interactive'],
});
const {
name,
value,
@@ -111,14 +117,7 @@ export default class EnvCreate extends EasCommand {
force,
type,
fileName,
} = await this.promptForMissingFlagsAsync(validatedFlags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvCreate, {
nonInteractive,
});
} = await this.promptForMissingFlagsAsync(validatedFlags, args, { graphqlClient, projectId });
const [projectDisplayName, ownerAccount] = await Promise.all([
getDisplayNameForProjectIdAsync(graphqlClient, projectId),
@@ -272,7 +271,8 @@ export default class EnvCreate extends EasCommand {
type,
...rest
}: CreateFlags,
{ environment }: { environment?: string }
{ environment }: { environment?: string },
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
): Promise<
Required<
Omit<CreateFlags, 'type' | 'visibility'> & {
@@ -323,18 +323,16 @@ export default class EnvCreate extends EasCommand {
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
if (environment && !isEnvironment(environment.toLowerCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
let newEnvironments = environments
? environments
: environment
? [environment.toLowerCase() as EnvironmentVariableEnvironment]
: undefined;
let newEnvironments = environments ? environments : environment ? [environment] : undefined;
if (!newEnvironments) {
newEnvironments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
newEnvironments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
canEnterCustomEnvironment: true,
graphqlClient,
projectId,
});
if (!newEnvironments || newEnvironments.length === 0) {
throw new Error('No environments selected');
+5 -12
View File
@@ -2,7 +2,6 @@ import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentVariableScopeFlag,
@@ -15,18 +14,18 @@ import { EnvironmentVariableMutation } from '../../graphql/mutations/Environment
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
import { formatVariableName } from '../../utils/variableUtils';
interface DeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
scope: EnvironmentVariableScope;
}
interface RawDeleteFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
scope: EASEnvironmentVariableScopeFlagValue;
}
@@ -38,7 +37,7 @@ export default class EnvDelete extends EasCommand {
'variable-name': Flags.string({
description: 'Name of the variable to delete',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to delete',
}),
@@ -50,7 +49,7 @@ export default class EnvDelete extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable to delete. One of 'production', 'preview', or 'development'.",
"Current environment of the variable to delete. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -166,12 +165,6 @@ export default class EnvDelete extends EasCommand {
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toLowerCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return { ...flags, 'variable-environment': environment, scope };
}
+9 -13
View File
@@ -1,24 +1,22 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
type ParsedFlags =
| {
nonInteractive: true;
environment: EnvironmentVariableEnvironment;
environment: string;
command: string;
}
| {
nonInteractive: false;
environment?: EnvironmentVariableEnvironment;
environment?: string;
command: string;
};
@@ -44,7 +42,7 @@ export default class EnvExec extends EasCommand {
name: 'environment',
required: true,
description:
"Environment to execute the command in. One of 'production', 'preview', or 'development'.",
"Environment to execute the command in. Default environments are 'production', 'preview', and 'development'.",
},
{
name: 'bash_command',
@@ -70,7 +68,11 @@ export default class EnvExec extends EasCommand {
const environment =
parsedFlags.environment ??
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
(await promptVariableEnvironmentAsync({
nonInteractive: parsedFlags.nonInteractive,
graphqlClient,
projectId,
}));
const environmentVariables = await this.loadEnvironmentVariablesAsync({
graphqlClient,
projectId,
@@ -101,12 +103,6 @@ export default class EnvExec extends EasCommand {
);
}
environment = environment?.toLowerCase();
if (!isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const firstChar = bash_command[0];
const lastChar = bash_command[bash_command.length - 1];
const cleanCommand =
@@ -198,7 +194,7 @@ export default class EnvExec extends EasCommand {
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
environment: EnvironmentVariableEnvironment;
environment: string;
nonInteractive: boolean;
}): Promise<Record<string, string>> {
const environmentVariablesQueryResult =
+8 -13
View File
@@ -1,7 +1,6 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -19,11 +18,11 @@ import {
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
import { formatVariable, formatVariableValue } from '../../utils/variableUtils';
interface RawGetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
format?: string;
scope: EASEnvironmentVariableScopeFlagValue;
@@ -31,7 +30,7 @@ interface RawGetFlags {
interface GetFlags {
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
format?: string;
scope: EnvironmentVariableScope;
@@ -49,7 +48,7 @@ export default class EnvGet extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable. One of 'production', 'preview', or 'development'.",
"Current environment of the variable. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -58,7 +57,7 @@ export default class EnvGet extends EasCommand {
'variable-name': Flags.string({
description: 'Name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable',
}),
@@ -93,6 +92,8 @@ export default class EnvGet extends EasCommand {
environment = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: false,
graphqlClient,
projectId,
});
}
@@ -156,12 +157,6 @@ export default class EnvGet extends EasCommand {
? EnvironmentVariableScope.Shared
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toLowerCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
...flags,
'variable-environment': environment,
@@ -177,7 +172,7 @@ async function getVariablesAsync(
scope: string,
projectId: string,
name: string | undefined,
environment: EnvironmentVariableEnvironment | undefined
environment: string | undefined
): Promise<EnvironmentVariableFragment[]> {
if (!name) {
throw new Error(
+11 -12
View File
@@ -1,7 +1,6 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -20,7 +19,6 @@ import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import {
formatVariable,
formatVariableValue,
isEnvironment,
performForEnvironmentsAsync,
} from '../../utils/variableUtils';
@@ -36,7 +34,7 @@ async function getVariablesForScopeAsync(
scope: EnvironmentVariableScope;
includingSensitive: boolean;
includeFileContent: boolean;
environment?: EnvironmentVariableEnvironment;
environment?: string;
projectId: string;
}
): Promise<EnvironmentVariableWithFileContent[]> {
@@ -71,7 +69,7 @@ async function getVariablesForScopeAsync(
interface RawListFlags {
scope: EASEnvironmentVariableScopeFlagValue;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
environment: string[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive'?: boolean;
@@ -80,7 +78,7 @@ interface RawListFlags {
interface ListFlags {
scope: EnvironmentVariableScope;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
environment: string[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive': boolean;
@@ -112,7 +110,7 @@ export default class EnvList extends EasCommand {
{
name: 'environment',
description:
"Environment to list the variables from. One of 'production', 'preview', or 'development'.",
"Environment to list the variables from. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -137,7 +135,12 @@ export default class EnvList extends EasCommand {
});
if (!environments) {
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
graphqlClient,
projectId,
});
}
await performForEnvironmentsAsync(environments, async environment => {
@@ -180,14 +183,10 @@ export default class EnvList extends EasCommand {
flags: RawListFlags,
{ environment }: { environment?: string }
): ListFlags {
if (environment && !isEnvironment(environment.toLowerCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments = flags.environment
? flags.environment
: environment
? [environment.toLowerCase() as EnvironmentVariableEnvironment]
? [environment]
: undefined;
return {
+9 -10
View File
@@ -13,7 +13,6 @@ import {
import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvPull extends EasCommand {
static override description =
@@ -29,7 +28,7 @@ export default class EnvPull extends EasCommand {
{
name: 'environment',
description:
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
"Environment to pull variables from. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -51,14 +50,6 @@ export default class EnvPull extends EasCommand {
let environment = flagEnvironment?.toLowerCase() ?? argEnvironment?.toLowerCase();
if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive });
}
if (!isEnvironment(environment)) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const {
projectId,
loggedIn: { graphqlClient },
@@ -67,6 +58,14 @@ export default class EnvPull extends EasCommand {
nonInteractive,
});
if (!environment) {
environment = await promptVariableEnvironmentAsync({
nonInteractive,
graphqlClient,
projectId,
});
}
targetPath = targetPath ?? '.env.local';
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
+56 -50
View File
@@ -3,7 +3,6 @@ import dotenv from 'dotenv';
import fs from 'fs-extra';
import path from 'path';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
import {
@@ -17,7 +16,6 @@ import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVari
import Log from '../../log';
import { confirmAsync, promptAsync } from '../../prompts';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import { isEnvironment } from '../../utils/variableUtils';
export default class EnvPush extends EasCommand {
static override description =
@@ -34,13 +32,17 @@ export default class EnvPush extends EasCommand {
description: 'Path to the input `.env` file',
default: '.env.local',
}),
force: Flags.boolean({
description: 'Skip confirmation and automatically override existing variables',
default: false,
}),
};
static override args = [
{
name: 'environment',
description:
"Environment to push variables to. One of 'production', 'preview', or 'development'.",
"Environment to push variables to. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -48,7 +50,7 @@ export default class EnvPush extends EasCommand {
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvPush);
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
let { environment: environments, path: envPath, force } = this.parseFlagsAndArgs(flags, args);
const {
projectId,
@@ -61,6 +63,9 @@ export default class EnvPush extends EasCommand {
environments = await promptVariableEnvironmentAsync({
nonInteractive: false,
multiple: true,
canEnterCustomEnvironment: true,
graphqlClient,
projectId,
});
}
@@ -114,40 +119,46 @@ export default class EnvPush extends EasCommand {
Log.warn(`Some variables already exist in the ${displayedEnvironment} environment.`);
const variableNames = existingDifferentVariables.map(variable => variable.name);
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
let variablesToOverwrite: string[] = [];
if (!confirm && existingDifferentVariables.length === 0) {
throw new Error('No new variables to push.');
}
if (confirm) {
if (force) {
// When --force is used, automatically override all existing variables
Log.log('Using --force flag: automatically overriding existing variables.');
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
const confirmationMessage =
variableNames.length > 1
? `The ${variableNames.join(
', '
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
const confirm = await confirmAsync({
message: confirmationMessage,
});
variablesToOverwrite = promptResult.variablesToOverwrite;
if (!confirm && existingDifferentVariables.length === 0) {
throw new Error('No new variables to push.');
}
if (confirm) {
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
} else {
const promptResult = await promptAsync({
type: 'multiselect',
name: 'variablesToOverwrite',
message: 'Select variables to overwrite:',
// @ts-expect-error property missing from `@types/prompts`
optionsPerPage: 20,
choices: existingDifferentVariables.map(variable => ({
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
variable.value ?? '(secret)'
})`,
value: variable.name,
})),
});
variablesToOverwrite = promptResult.variablesToOverwrite;
}
}
for (const existingVariable of existingVariables) {
@@ -165,7 +176,7 @@ export default class EnvPush extends EasCommand {
variable => variable.visibility !== EnvironmentVariableVisibility.Public
);
if (existingSensitiveVariables.length > 0) {
if (existingSensitiveVariables.length > 0 && !force) {
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
variable => `- ${variable.name}`
);
@@ -177,6 +188,8 @@ export default class EnvPush extends EasCommand {
if (!confirm) {
throw new Error('Aborting...');
}
} else if (existingSensitiveVariables.length > 0 && force) {
Log.log('Using --force flag: automatically overriding sensitive variables.');
}
}
@@ -196,16 +209,14 @@ export default class EnvPush extends EasCommand {
}
parseFlagsAndArgs(
flags: { path: string; environment: EnvironmentVariableEnvironment[] | undefined },
flags: {
path: string;
environment: string[] | undefined;
force: boolean;
},
{ environment }: Record<string, string>
): { environment?: EnvironmentVariableEnvironment[]; path: string } {
if (environment && !isEnvironment(environment.toLowerCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}
const environments =
flags.environment ??
(environment ? [environment.toLowerCase() as EnvironmentVariableEnvironment] : undefined);
): { environment?: string[]; path: string; force: boolean } {
const environments = flags.environment ?? (environment ? [environment] : undefined);
return {
...flags,
@@ -215,13 +226,8 @@ export default class EnvPush extends EasCommand {
private async parseEnvFileAsync(
envPath: string,
environments: EnvironmentVariableEnvironment[]
): Promise<
Record<
string,
CreateEnvironmentVariableInput & { environments: EnvironmentVariableEnvironment[] }
>
> {
environments: string[]
): Promise<Record<string, CreateEnvironmentVariableInput & { environments: string[] }>> {
if (!(await fs.exists(envPath))) {
throw new Error(`File ${envPath} does not exist.`);
}
+26 -24
View File
@@ -4,8 +4,8 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
@@ -36,17 +36,17 @@ import {
promptVariableValueAsync,
promptVariableVisibilityAsync,
} from '../../utils/prompts';
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
import { formatVariableName } from '../../utils/variableUtils';
interface RawUpdateFlags {
name?: string;
value?: string;
scope: EASEnvironmentVariableScopeFlagValue;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
}
@@ -54,11 +54,11 @@ interface UpdateFlags {
name?: string;
value?: string;
scope: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
environment?: string[];
visibility?: 'plaintext' | 'sensitive' | 'secret';
type?: 'string' | 'file';
'variable-name'?: string;
'variable-environment'?: EnvironmentVariableEnvironment;
'variable-environment'?: string;
'non-interactive': boolean;
}
@@ -69,7 +69,7 @@ export default class EnvUpdate extends EasCommand {
'variable-name': Flags.string({
description: 'Current name of the variable',
}),
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable to update',
}),
@@ -93,7 +93,7 @@ export default class EnvUpdate extends EasCommand {
{
name: 'environment',
description:
"Current environment of the variable to update. One of 'production', 'preview', or 'development'.",
"Current environment of the variable to update. Default environments are 'production', 'preview', and 'development'.",
required: false,
},
];
@@ -180,15 +180,19 @@ export default class EnvUpdate extends EasCommand {
visibility: newVisibility,
type: newType,
fileName,
} = await this.promptForMissingFlagsAsync(selectedVariable, {
name,
value: rawValue,
environment: environments,
visibility,
'non-interactive': nonInteractive,
type,
scope,
});
} = await this.promptForMissingFlagsAsync(
selectedVariable,
{
name,
value: rawValue,
environment: environments,
visibility,
'non-interactive': nonInteractive,
type,
scope,
},
{ graphqlClient, projectId }
);
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
id: selectedVariable.id,
@@ -226,12 +230,6 @@ export default class EnvUpdate extends EasCommand {
: EnvironmentVariableScope.Project;
if (environment) {
environment = environment.toLowerCase();
if (!isEnvironment(environment)) {
throw new Error(
"Invalid environment. Use one of 'production', 'preview', or 'development'."
);
}
return {
...flags,
'variable-environment': environment,
@@ -252,7 +250,8 @@ export default class EnvUpdate extends EasCommand {
'non-interactive': nonInteractive,
type,
...rest
}: UpdateFlags
}: UpdateFlags,
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
): Promise<
Omit<UpdateFlags, 'type' | 'visibility'> & {
type?: EnvironmentSecretType;
@@ -316,7 +315,10 @@ export default class EnvUpdate extends EasCommand {
environments = await promptVariableEnvironmentAsync({
nonInteractive,
multiple: true,
canEnterCustomEnvironment: true,
selectedEnvironments: selectedVariable.environments ?? [],
graphqlClient,
projectId,
});
if (
@@ -4,7 +4,6 @@ import chalk from 'chalk';
import { getExpoWebsiteBaseUrl } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
import { GetServerSideEnvironmentVariablesFn } from '../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
@@ -97,7 +96,7 @@ export default class FingerprintCompare extends EasCommand {
open: Flags.boolean({
description: 'Open the fingerprint comparison in the browser',
}),
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
description:
'If generating a fingerprint from the local directory, use the specified environment.',
@@ -4,7 +4,6 @@ import { Flags } from '@oclif/core';
import { getExpoWebsiteBaseUrl } from '../../api';
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import EasCommand from '../../commandUtils/EasCommand';
import {
EASEnvironmentFlag,
@@ -40,7 +39,7 @@ export default class FingerprintGenerate extends EasCommand {
options: ['android', 'ios'],
}),
...EASEnvironmentFlag,
environment: Flags.enum<EnvironmentVariableEnvironment>({
environment: Flags.string({
...EasEnvironmentFlagParameters,
exclusive: ['build-profile'],
}),
@@ -39,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
vcsClient,
} = await this.getContextAsync(UpdateConfigure, {
nonInteractive: flags['non-interactive'],
withServerSideEnvironment: flags['environment'],
withServerSideEnvironment: flags['environment'] ?? null,
});
Log.log(
@@ -6,7 +6,6 @@ import nullthrows from 'nullthrows';
// import { getExpoWebsiteBaseUrl } from '../../api';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
@@ -79,7 +78,7 @@ type RawUpdateFlags = {
'rollout-percentage'?: number;
'non-interactive': boolean;
json: boolean;
environment: EnvironmentVariableEnvironment | null;
environment?: string;
};
type UpdateFlags = {
@@ -96,7 +95,7 @@ type UpdateFlags = {
rolloutPercentage?: number;
json: boolean;
nonInteractive: boolean;
environment: EnvironmentVariableEnvironment | null;
environment?: string;
};
export default class UpdatePublish extends EasCommand {
@@ -193,7 +192,7 @@ export default class UpdatePublish extends EasCommand {
getServerSideEnvironmentVariablesAsync,
} = await this.getContextAsync(UpdatePublish, {
nonInteractive,
withServerSideEnvironment: environment,
withServerSideEnvironment: environment ?? null,
});
if (jsonFlag) {
+7
View File
@@ -10079,6 +10079,13 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> }, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> } } };
export type AppEnvironmentVariableEnvironmentsQueryVariables = Exact<{
appId: Scalars['String']['input'];
}>;
export type AppEnvironmentVariableEnvironmentsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariableEnvironments: Array<any> } } };
export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{
appId: Scalars['String']['input'];
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
@@ -1,7 +1,6 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
@@ -14,7 +13,7 @@ import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
import { EnvironmentVariableWithSecretFragmentNode } from '../types/EnvironmentVariableWithSecret';
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
linkedEnvironments?: string[] | null;
};
export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
@@ -22,6 +21,31 @@ export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
};
export const EnvironmentVariablesQuery = {
async environmentVariableEnvironmentsAsync(
graphqlClient: ExpoGraphqlClient,
appId: string
): Promise<string[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query(
gql`
query AppEnvironmentVariableEnvironments($appId: String!) {
app {
byId(appId: $appId) {
id
environmentVariableEnvironments
}
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);
return data.app?.byId.environmentVariableEnvironments ?? [];
},
async byAppIdWithSensitiveAsync(
graphqlClient: ExpoGraphqlClient,
{
@@ -31,7 +55,7 @@ export const EnvironmentVariablesQuery = {
includeFileContent = false,
}: {
appId: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
filterNames?: string[];
includeFileContent?: boolean;
}
@@ -80,7 +104,7 @@ export const EnvironmentVariablesQuery = {
includeFileContent = false,
}: {
appId: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
filterNames?: string[];
includeFileContent?: boolean;
}
@@ -128,7 +152,7 @@ export const EnvironmentVariablesQuery = {
}: {
appId: string;
filterNames?: string[];
environment?: EnvironmentVariableEnvironment;
environment?: string;
includeFileContent?: boolean;
}
): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> {
@@ -178,7 +202,7 @@ export const EnvironmentVariablesQuery = {
}: {
appId: string;
filterNames?: string[];
environment?: EnvironmentVariableEnvironment;
environment?: string;
includeFileContent?: boolean;
}
): Promise<EnvironmentVariableWithFileContent[]> {
+104 -21
View File
@@ -1,10 +1,31 @@
import chalk from 'chalk';
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
import { DefaultEnvironment } from '../build/utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import { RequestedPlatform } from '../platform';
import { promptAsync, selectAsync } from '../prompts';
const DEFAULT_ENVIRONMENTS = Object.values(DefaultEnvironment);
export async function getProjectEnvironmentVariableEnvironmentsAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string
): Promise<string[]> {
try {
const environments = await EnvironmentVariablesQuery.environmentVariableEnvironmentsAsync(
graphqlClient,
projectId
);
return environments;
} catch {
throw new Error('Failed to fetch available environments');
}
}
const CUSTOM_ENVIRONMENT_VALUE = '~~CUSTOM~~';
export async function promptVariableTypeAsync(
nonInteractive: boolean,
initialType?: EnvironmentSecretType
@@ -44,6 +65,24 @@ export function parseVisibility(
}
}
async function promptCustomEnvironmentAsync(): Promise<string> {
const { customEnvironment } = await promptAsync({
type: 'text',
name: 'customEnvironment',
message: 'Enter custom environment name:',
validate: (value: string) => {
if (!value || value.trim() === '') {
return 'Environment name cannot be empty';
}
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
return 'Environment name may only contain letters, numbers, underscores, and hyphens';
}
return true;
},
});
return customEnvironment;
}
export async function promptVariableVisibilityAsync(
nonInteractive: boolean,
selectedVisibility?: EnvironmentVariableVisibility | null
@@ -74,49 +113,93 @@ export async function promptVariableVisibilityAsync(
type EnvironmentPromptArgs = {
nonInteractive: boolean;
selectedEnvironments?: EnvironmentVariableEnvironment[];
availableEnvironments?: EnvironmentVariableEnvironment[];
selectedEnvironments?: string[];
graphqlClient?: ExpoGraphqlClient;
projectId?: string;
canEnterCustomEnvironment?: boolean;
};
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple: true }
): Promise<EnvironmentVariableEnvironment[]>;
): Promise<string[]>;
export function promptVariableEnvironmentAsync(
input: EnvironmentPromptArgs & { multiple?: false }
): Promise<EnvironmentVariableEnvironment>;
): Promise<string>;
export async function promptVariableEnvironmentAsync({
nonInteractive,
selectedEnvironments,
multiple = false,
availableEnvironments,
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<
EnvironmentVariableEnvironment[] | EnvironmentVariableEnvironment
> {
canEnterCustomEnvironment = false,
graphqlClient,
projectId,
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<string[] | string> {
if (nonInteractive) {
throw new Error(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
}
if (!multiple) {
return await selectAsync(
'Select environment:',
(availableEnvironments ?? Object.values(EnvironmentVariableEnvironment)).map(environment => ({
title: environment.toLocaleLowerCase(),
value: environment,
}))
let allEnvironments: string[] = DEFAULT_ENVIRONMENTS;
if (graphqlClient && projectId) {
const projectEnvironments = await getProjectEnvironmentVariableEnvironmentsAsync(
graphqlClient,
projectId
);
allEnvironments = [...new Set([...DEFAULT_ENVIRONMENTS, ...projectEnvironments])];
}
if (!multiple) {
const choices = allEnvironments.map(environment => ({
title: environment,
value: environment,
}));
if (canEnterCustomEnvironment) {
choices.push({
title: 'Other (enter custom environment)',
value: CUSTOM_ENVIRONMENT_VALUE,
});
}
const selectedEnvironment = await selectAsync('Select environment:', choices);
if (selectedEnvironment === CUSTOM_ENVIRONMENT_VALUE) {
return await promptCustomEnvironmentAsync();
}
return selectedEnvironment;
}
const choices = allEnvironments.map(environment => ({
title: environment,
value: environment,
selected: selectedEnvironments?.includes(environment),
}));
if (canEnterCustomEnvironment) {
choices.push({
title: 'Other (enter custom environment)',
value: CUSTOM_ENVIRONMENT_VALUE,
selected: false,
});
}
const { environments } = await promptAsync({
message: 'Select environment:',
name: 'environments',
type: 'multiselect',
choices: Object.values(EnvironmentVariableEnvironment).map(environment => ({
title: environment.toLocaleLowerCase(),
value: environment,
selected: selectedEnvironments?.includes(environment),
})),
choices,
});
if (environments?.includes(CUSTOM_ENVIRONMENT_VALUE)) {
const customEnvironment = await promptCustomEnvironmentAsync();
const filteredEnvironments = environments.filter(
(env: string) => env !== CUSTOM_ENVIRONMENT_VALUE
);
return [...filteredEnvironments, customEnvironment];
}
return environments;
}
+2 -9
View File
@@ -1,7 +1,6 @@
import dateFormat from 'dateformat';
import formatFields from './formatFields';
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
import {
EnvironmentSecretType,
EnvironmentVariableFragment,
@@ -10,12 +9,6 @@ import {
} from '../graphql/generated';
import { EnvironmentVariableWithFileContent } from '../graphql/queries/EnvironmentVariablesQuery';
export function isEnvironment(environment: string): environment is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
environment as EnvironmentVariableEnvironment
);
}
export function formatVariableName(variable: EnvironmentVariableFragment): string {
const name = variable.name;
const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared';
@@ -53,8 +46,8 @@ export function formatVariableValue(variable: EnvironmentVariableWithFileContent
}
export async function performForEnvironmentsAsync(
environments: EnvironmentVariableEnvironment[] | null,
fun: (environment: EnvironmentVariableEnvironment | undefined) => Promise<any>
environments: string[] | null,
fun: (environment: string | undefined) => Promise<any>
): Promise<any[]> {
const selectedEnvironments = environments ?? [undefined];
return await Promise.all(selectedEnvironments.map(env => fun(env)));
+1 -2
View File
@@ -8,7 +8,6 @@ import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { pack } from 'tar-stream';
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
@@ -191,7 +190,7 @@ export interface CreateManifestResult {
interface CreateManifestParams {
projectId: string;
projectDir: string;
environment?: EnvironmentVariableEnvironment;
environment?: string;
}
/** Creates a manifest configuration sent up for deployment */
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "16.20.1",
"version": "16.23.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
+2 -1
View File
@@ -214,7 +214,8 @@
"environment": {
"description": "Environment to use for the server-side defined EAS environment variables during build process and command execution. Learn more: https://docs.expo.dev/eas/environment-variables/",
"markdownDescription": "Environment to use for the server-side defined EAS environment variables during build process and command execution. [Learn more](https://docs.expo.dev/eas/environment-variables/)",
"enum": ["production", "preview", "development"]
"examples": ["production", "preview", "development"],
"type": "string"
},
"android": {
"$ref": "#/definitions/BuildProfileAndroid"
@@ -129,6 +129,35 @@ test('valid eas.json with specified environment', async () => {
});
});
test('supports custom environment names', async () => {
await fs.writeJson('/project/eas.json', {
build: {
development: {
environment: 'my-custom-environment',
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'development');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'development'
);
expect(androidProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
environment: 'my-custom-environment',
});
expect(iosProfile).toEqual({
credentialsSource: 'remote',
distribution: 'store',
environment: 'my-custom-environment',
});
});
test('valid eas.json with specified keystoreName', async () => {
const keystoreName = 'this-is-a-keystore-name';
await fs.writeJson('/project/eas.json', {
+9 -1
View File
@@ -71,7 +71,15 @@ const CommonBuildProfileSchema = Joi.object({
// credentials
withoutCredentials: Joi.boolean(),
environment: Joi.string().valid('preview', 'production', 'development'),
environment: Joi.string()
.min(3)
.max(100)
.pattern(/^[a-z0-9_-]+$/)
.messages({
'string.min': 'Environment name must be at least 3 characters.',
'string.max': 'Environment name must not be longer than 100 characters.',
'string.pattern.base': 'Environment name can only contain lowercase letters, numbers, dashes and underscores.',
}),
});
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
+1 -1
View File
@@ -71,7 +71,7 @@ export interface CommonBuildProfile {
// credentials
withoutCredentials?: boolean;
environment?: 'preview' | 'production' | 'development';
environment?: string;
}
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
Generated
+10
View File
@@ -1549,6 +1549,16 @@
semver "^7.6.2"
zod "^4.1.3"
"@expo/[email protected]":
version "1.0.237"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.237.tgz#2a3230540ac85599ee51d01b62c9d7b74ccfad2a"
integrity sha512-bvE7/+QO6hcyeD7Jn5job4jRZJNLep7MDaOsYTZ66xRNYiqY8g2TMKTcg8oOaBvhv8i5WJMJOexNmzkxWFjUtw==
dependencies:
"@expo/logger" "1.0.221"
joi "^17.13.1"
semver "^7.6.2"
zod "^4.1.3"
"@expo/env@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/env/-/env-1.0.0.tgz#8e22894fae04ba6dc41fe0f1351a4973d410273a"