Compare commits

...
Author SHA1 Message Date
Krystof WoldrichandCodex 076585b7f0 [eas-cli] Clarify environment flag requirements and errors
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 16:01:52 +02:00
Krystof WoldrichandCodex dc6a5a7ec6 fix review comments
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex d19a84d246 fix env:update variable-environment is not required in non interactive
mode

Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex b4b05dc5fa Add env help changelog entry
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex 3c3574ab79 Refine env non-interactive help
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex 729a01fe64 Support extending grouped flag descriptions
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex 72a511017e Revert "Require scope for env:get non-interactive mode"
This reverts commit 36c58a8b130b70f06607349dca417c8618eea199.

Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex a58609e09f Require scope for env:get non-interactive mode
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex 29f102c367 Clarify env command non-interactive usage
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
Krystof WoldrichandCodex 06563efbb9 Clarify env:create non-interactive usage
Co-authored-by: Codex (GPT-6) <[email protected]>
2026-09-07 15:38:25 +02:00
17 changed files with 445 additions and 60 deletions

No files matched your search

+2
View File
@@ -16,6 +16,8 @@ This is the log of notable changes to EAS CLI and related packages.
- [build-tools] Reduce `expo-device-hub` preview resolution from 1280 px to 960 px to match `serve-sim` and lower streaming bandwidth. ([#4326](https://github.com/expo/eas-cli/pull/4326) by [@krystofwoldrich-agent](https://github.com/krystofwoldrich-agent))
- [build-tools] Install FFmpeg before launching `expo-device-hub` in Android device sessions. ([#4332](https://github.com/expo/eas-cli/pull/4332) by [@krystofwoldrich-agent](https://github.com/krystofwoldrich-agent))
- [eas-cli] Improve `eas env:*` non-interactive required input help and error messages. ([#3820](https://github.com/expo/eas-cli/pull/3820) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- [eas-cli] Reject conflicting positional `ENVIRONMENT` and `--variable-environment` inputs in `eas env:update` and `eas env:delete`. ([#3820](https://github.com/expo/eas-cli/pull/3820) by [@krystofwoldrich](https://github.com/krystofwoldrich))
### 🧹 Chores
@@ -1,4 +1,5 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { boolish } from 'getenv';
export function isNonInteractiveByDefault(): boolean {
@@ -26,6 +27,65 @@ export function resolveNonInteractiveAndJsonFlags(flags: {
return { json, nonInteractive };
}
export function prependFlagDescription<T extends Record<string, { description?: string }>>(
flags: T,
extraDescription: string
): T {
return Object.fromEntries(
Object.entries(flags).map(([name, flag]) => {
const description = flag.description ?? '';
return [
name,
{
...flag,
description: description ? `${extraDescription} ${description}` : extraDescription,
},
];
})
) as T;
}
export function markRequiredInNonInteractiveMode<
T extends Record<string, { description?: string }>,
>(flags: T): T {
return prependFlagDescription(flags, '(required in non-interactive mode)');
}
export function validateNonInteractiveRequiredInputs({
nonInteractive,
requiredInputs,
helpCommand,
}: {
nonInteractive: boolean;
requiredInputs: { if?: boolean; name: string; value: unknown }[];
helpCommand: string;
}): void {
if (!nonInteractive) {
return;
}
const missingInputs = requiredInputs
.filter(({ if: condition = true, value }) => {
if (!condition) {
return false;
}
if (Array.isArray(value)) {
return value.length === 0;
}
return !value;
})
.map(({ name }) => name);
if (missingInputs.length === 0) {
return;
}
throw new Error(
`Missing required inputs for non-interactive mode: ${missingInputs.join(', ')}.\n` +
`Run ${chalk.bold(helpCommand)} for usage and examples.`
);
}
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
};
@@ -365,6 +365,19 @@ describe(EnvCreate, () => {
});
});
describe('in non-interactive mode', () => {
it('reports missing required inputs before loading command context', async () => {
const command = new EnvCreate(['--non-interactive'], mockConfig);
// @ts-expect-error
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync');
await expect(command.runAsync()).rejects.toThrow(
/Missing required inputs for non-interactive mode: --name, --value, --visibility, --environment\.[\s\S]*eas env:create --help/
);
expect(getContextAsyncSpy).not.toHaveBeenCalled();
});
});
it('accepts development environment when using positional argument', async () => {
const command = new EnvCreate(
[
@@ -106,7 +106,7 @@ describe(EnvDelete, () => {
);
});
it('cancels deletion when user does not confirm', async () => {
it('reports missing variable name in non-interactive mode before loading command context', async () => {
const mockVariables = [
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
];
@@ -117,9 +117,12 @@ describe(EnvDelete, () => {
const command = new EnvDelete(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrowErrorMatchingSnapshot();
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
await expect(command.runAsync()).rejects.toThrow(
/Missing required inputs for non-interactive mode: --variable-name\.[\s\S]*eas env:delete --help/
);
expect(getContextAsyncSpy).not.toHaveBeenCalled();
expect(EnvironmentVariableMutation.deleteAsync).not.toHaveBeenCalled();
});
@@ -158,4 +161,46 @@ describe(EnvDelete, () => {
environment: DefaultEnvironment.Development,
});
});
it('rejects positional environment and --variable-environment used together', async () => {
const mockVariable = {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
jest.mocked(EnvironmentVariableMutation.deleteAsync).mockResolvedValueOnce({ id: 'var1' });
const command = new EnvDelete(
[
'development',
'--variable-name',
'TEST_VAR_1',
'--variable-environment',
'production',
'--scope',
'project',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId,
});
await expect(command.runAsync()).rejects.toThrow(
"You can't use the --variable-environment flag when the environment is passed as a positional argument. Run `eas env:delete --help` for more information."
);
expect(getContextAsyncSpy).not.toHaveBeenCalled();
});
});
@@ -66,6 +66,21 @@ describe(EnvList, () => {
);
});
it('reports missing environment in non-interactive mode', async () => {
const command = new EnvList(['--non-interactive'], mockConfig);
// @ts-expect-error
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
});
await expect(command.runAsync()).rejects.toThrow(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
expect(getContextAsyncSpy).toHaveBeenCalled();
});
it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
@@ -281,7 +281,7 @@ describe(EnvPull, () => {
const command = new EnvPull(['--non-interactive'], mockConfig);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
@@ -290,6 +290,7 @@ describe(EnvPull, () => {
await expect(command.runAsync()).rejects.toThrow(
'The `--environment` flag must be set when running in `--non-interactive` mode.'
);
expect(getContextAsyncSpy).toHaveBeenCalled();
});
});
@@ -89,6 +89,52 @@ SECRET_KEY=super-secret-key`;
});
});
it('reports missing environment in non-interactive mode', async () => {
const command = new EnvPush(['--non-interactive', '--path', testEnvPath], mockConfig);
// @ts-expect-error
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
/The `--environment` flag must be set when running in `--non-interactive` mode/
);
expect(getContextAsyncSpy).toHaveBeenCalled();
});
it('allows non-interactive mode without --force when nothing needs confirmation', async () => {
const command = new EnvPush(
['--environment', 'development', '--path', testEnvPath, '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await command.runAsync();
expect(confirmAsync).not.toHaveBeenCalled();
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).toHaveBeenCalledWith(
graphqlClient,
expect.arrayContaining([
expect.objectContaining({
name: 'VARIABLE_NAME',
environments: [DefaultEnvironment.Development],
}),
]),
testProjectId
);
});
describe('--force option', () => {
it('automatically overrides existing variables without confirmation when --force is used', async () => {
const existingVariable = {
@@ -275,5 +321,79 @@ SECRET_KEY=super-secret-key`;
'Using --force flag: automatically overriding sensitive variables.'
);
});
it('throws before confirming overwrite in non-interactive mode 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.Public,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
const command = new EnvPush(
['--environment', 'development', '--path', testEnvPath, '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
'Cannot confirm overwriting existing variables in non-interactive mode. Use --force to overwrite them.'
);
expect(confirmAsync).not.toHaveBeenCalled();
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).not.toHaveBeenCalled();
});
it('throws before confirming sensitive overwrite in non-interactive mode when --force is not used', async () => {
const existingSensitiveVariable = {
id: 'var1',
name: 'SECRET_KEY',
value: 'super-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(
['--environment', 'development', '--path', testEnvPath, '--non-interactive'],
mockConfig
);
// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId: testProjectId,
projectDir: testProjectDir,
});
await expect(command.runAsync()).rejects.toThrow(
'Cannot confirm overwriting sensitive variables in non-interactive mode. Use --force to overwrite them.'
);
expect(confirmAsync).not.toHaveBeenCalled();
expect(
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
).not.toHaveBeenCalled();
});
});
});
@@ -253,6 +253,50 @@ describe(EnvUpdate, () => {
});
});
it('rejects positional environment and --variable-environment used together', async () => {
const mockVariable = {
id: 'var1',
name: 'TEST_VAR_1',
value: 'value1',
environments: [DefaultEnvironment.Development],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
};
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce([mockVariable]);
jest.mocked(EnvironmentVariableMutation.updateAsync).mockResolvedValueOnce(mockVariable);
const command = new EnvUpdate(
[
'development',
'--variable-name',
'TEST_VAR_1',
'--variable-environment',
'production',
'--value',
'new-value',
'--scope',
'project',
'--non-interactive',
],
mockConfig
);
// @ts-expect-error
const getContextAsyncSpy = jest.spyOn(command, 'getContextAsync').mockReturnValue({
loggedIn: { graphqlClient },
projectId,
});
await expect(command.runAsync()).rejects.toThrow(
"You can't use the --variable-environment flag when the environment is passed as a positional argument. Run `eas env:update --help` for more information."
);
expect(getContextAsyncSpy).not.toHaveBeenCalled();
});
it('processes file type variable in non-interactive mode with --type file and --value', async () => {
const testFilePath = '/path/to/test-file.json';
const testFileBase64 = 'dGVzdCBmaWxlIGNvbnRlbnQ=';
@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnvDelete cancels deletion when user does not confirm 1`] = `"Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with --variable-name VARIABLE_NAME flag to fix the issue"`;
+27 -5
View File
@@ -11,6 +11,8 @@ import {
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableVisibilityFlag,
markRequiredInNonInteractiveMode,
validateNonInteractiveRequiredInputs,
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
@@ -61,6 +63,11 @@ export default class EnvCreate extends EasCommand {
'create an environment variable for the current project or account (deprecated, use eas env:set)';
static override hidden = true;
static override examples = [
'$ eas env:create --environment production --environment preview --name API_TOKEN --value "$API_TOKEN" --visibility sensitive',
'$ eas env:create --environment production --scope account --name SHARED_TOKEN --value "$SHARED_TOKEN" --visibility secret',
];
static override args = {
environment: Args.string({
description:
@@ -71,22 +78,24 @@ export default class EnvCreate extends EasCommand {
static override flags = {
name: Flags.string({
description: 'Name of the variable',
description: '(required in non-interactive mode) Name of the variable',
}),
value: Flags.string({
description: 'Text value or the variable',
description:
'(required in non-interactive mode) Text value of the variable, or a file path when --type=file',
}),
force: Flags.boolean({
description: 'Overwrite existing variable',
description:
'(required when overwriting in non-interactive mode) Overwrite existing variable',
default: false,
}),
type: Flags.option({
description: 'The type of variable',
options: ['string', 'file'] as const,
})(),
...EASVariableVisibilityFlag,
...markRequiredInNonInteractiveMode(EASVariableVisibilityFlag),
...EASEnvironmentVariableScopeFlag,
...EASMultiEnvironmentFlag,
...markRequiredInNonInteractiveMode(EASMultiEnvironmentFlag),
...EASNonInteractiveFlag,
};
@@ -103,6 +112,19 @@ export default class EnvCreate extends EasCommand {
const { args, flags } = await this.parse(EnvCreate);
const validatedFlags = this.sanitizeFlags(flags);
validateNonInteractiveRequiredInputs({
nonInteractive: validatedFlags['non-interactive'],
requiredInputs: [
{ name: '--name', value: validatedFlags.name },
{ name: '--value', value: validatedFlags.value },
{ name: '--visibility', value: validatedFlags.visibility },
{
name: '--environment',
value: args.environment ?? validatedFlags.environment,
},
],
helpCommand: 'eas env:create --help',
});
const {
projectId,
+16 -10
View File
@@ -1,6 +1,5 @@
import { Args, Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import {
@@ -8,6 +7,7 @@ import {
EASEnvironmentVariableScopeFlagValue,
EASNonInteractiveFlag,
EasEnvironmentFlagParameters,
validateNonInteractiveRequiredInputs,
} from '../../commandUtils/flags';
import { EnvironmentVariableScope } from '../../graphql/generated';
import { EnvironmentVariableMutation } from '../../graphql/mutations/EnvironmentVariableMutation';
@@ -33,9 +33,14 @@ interface RawDeleteFlags {
export default class EnvDelete extends EasCommand {
static override description = 'delete an environment variable for the current project or account';
static override examples = [
'$ eas env:delete --variable-environment production --variable-name API_TOKEN',
'$ eas env:delete --scope account --variable-name SHARED_TOKEN',
];
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable to delete',
description: '(required in non-interactive mode) Name of the variable to delete',
}),
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
@@ -148,14 +153,15 @@ export default class EnvDelete extends EasCommand {
flags: RawDeleteFlags,
{ environment }: { environment?: string }
): DeleteFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error(
`Environment variable needs 'name' to be specified when running in non-interactive mode. Run the command with ${chalk.bold(
'--variable-name VARIABLE_NAME'
)} flag to fix the issue`
);
}
validateNonInteractiveRequiredInputs({
nonInteractive: flags['non-interactive'],
requiredInputs: [{ name: '--variable-name', value: flags['variable-name'] }],
helpCommand: 'eas env:delete --help',
});
if (environment && flags['variable-environment']) {
throw new Error(
"You can't use the --variable-environment flag when the environment is passed as a positional argument. Run `eas env:delete --help` for more information."
);
}
const scope =
+2
View File
@@ -29,6 +29,8 @@ export default class EnvExec extends EasCommand {
static override description =
'execute a command with environment variables from the selected environment';
static override examples = ["$ eas env:exec production 'bun run build'"];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
+20 -14
View File
@@ -9,6 +9,7 @@ import {
EASNonInteractiveFlag,
EASVariableFormatFlag,
EasEnvironmentFlagParameters,
validateNonInteractiveRequiredInputs,
} from '../../commandUtils/flags';
import {
EnvironmentVariableFragment,
@@ -39,6 +40,11 @@ interface GetFlags {
export default class EnvGet extends EasCommand {
static override description = 'view an environment variable for the current project or account';
static override examples = [
'$ eas env:get --variable-environment production --variable-name API_URL',
'$ eas env:get --variable-environment production --variable-name API_TOKEN --format long',
];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
@@ -54,11 +60,11 @@ export default class EnvGet extends EasCommand {
static override flags = {
'variable-name': Flags.string({
description: 'Name of the variable',
description: '(required) Name of the variable',
}),
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
description: 'Current environment of the variable',
description: '(required in non-interactive mode) Current environment of the variable',
}),
...EASVariableFormatFlag,
...EASEnvironmentVariableScopeFlag,
@@ -134,20 +140,20 @@ export default class EnvGet extends EasCommand {
}
private sanitizeInputs(flags: RawGetFlags, { environment }: { environment?: string }): GetFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error('Variable name is required. Run the command with --variable-name flag.');
}
if (!flags.scope) {
throw new Error('Scope is required. Run the command with --scope flag.');
}
if (!(flags['variable-environment'] ?? environment)) {
throw new Error('Environment is required.');
}
}
validateNonInteractiveRequiredInputs({
nonInteractive: flags['non-interactive'],
requiredInputs: [
{ name: '--variable-name', value: flags['variable-name'] },
{
name: '--variable-environment',
value: environment ?? flags['variable-environment'],
},
],
helpCommand: 'eas env:get --help',
});
if (environment && flags['variable-environment']) {
throw new Error(
"You can't use both --variable-environment flag when environment is passed as an argument. Run `eas env:get --help` for more information."
"You can't use the --variable-environment flag when the environment is passed as a positional argument. Run `eas env:get --help` for more information."
);
}
+7 -3
View File
@@ -7,7 +7,9 @@ import {
EASEnvironmentVariableScopeFlag,
EASEnvironmentVariableScopeFlagValue,
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableFormatFlag,
markRequiredInNonInteractiveMode,
} from '../../commandUtils/flags';
import { EnvironmentVariableScope } from '../../graphql/generated';
import {
@@ -72,7 +74,7 @@ interface RawListFlags {
environment: string[] | undefined;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive'?: boolean;
'non-interactive': boolean;
}
interface ListFlags {
@@ -87,6 +89,8 @@ interface ListFlags {
export default class EnvList extends EasCommand {
static override description = 'list environment variables for the current project or account';
static override examples = ['$ eas env:list --environment production'];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
@@ -101,9 +105,10 @@ export default class EnvList extends EasCommand {
description: 'Display files content in the output',
default: false,
}),
...EASMultiEnvironmentFlag,
...markRequiredInNonInteractiveMode(EASMultiEnvironmentFlag),
...EASVariableFormatFlag,
...EASEnvironmentVariableScopeFlag,
...EASNonInteractiveFlag,
};
static override args = {
@@ -186,7 +191,6 @@ export default class EnvList extends EasCommand {
return {
...flags,
'non-interactive': flags['non-interactive'] ?? false,
environment: environments,
scope:
flags.scope === 'account'
+11 -2
View File
@@ -4,7 +4,11 @@ import * as fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags';
import {
EASEnvironmentFlag,
EASNonInteractiveFlag,
markRequiredInNonInteractiveMode,
} from '../../commandUtils/flags';
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../../graphql/generated';
import {
EnvironmentVariableWithFileContent,
@@ -18,6 +22,11 @@ export default class EnvPull extends EasCommand {
static override description =
'pull environment variables for the selected environment to .env file';
static override examples = [
'$ eas env:pull --environment development',
'$ eas env:pull --environment production --path .env.production',
];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
@@ -34,7 +43,7 @@ export default class EnvPull extends EasCommand {
static override flags = {
...EASNonInteractiveFlag,
...EASEnvironmentFlag,
...markRequiredInNonInteractiveMode(EASEnvironmentFlag),
path: Flags.string({
description: 'Path to the result `.env` file',
default: '.env.local',
+33 -6
View File
@@ -4,7 +4,11 @@ import fs from 'fs-extra';
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
import {
EASMultiEnvironmentFlag,
EASNonInteractiveFlag,
markRequiredInNonInteractiveMode,
} from '../../commandUtils/flags';
import {
CreateEnvironmentVariableInput,
EnvironmentVariableFragment,
@@ -21,13 +25,17 @@ export default class EnvPush extends EasCommand {
static override description =
'push environment variables from .env file to the selected environment';
static override examples = [
'$ eas env:push --environment production --path .env.production --force',
];
static override contextDefinition = {
...this.ContextOptions.ProjectId,
...this.ContextOptions.LoggedIn,
};
static override flags = {
...EASMultiEnvironmentFlag,
...markRequiredInNonInteractiveMode(EASMultiEnvironmentFlag),
path: Flags.string({
description: 'Path to the input `.env` file',
default: '.env.local',
@@ -36,6 +44,7 @@ export default class EnvPush extends EasCommand {
description: 'Skip confirmation and automatically override existing variables',
default: false,
}),
...EASNonInteractiveFlag,
};
static override args = {
@@ -49,18 +58,23 @@ export default class EnvPush extends EasCommand {
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvPush);
let { environment: environments, path: envPath, force } = this.parseFlagsAndArgs(flags, args);
let {
environment: environments,
path: envPath,
force,
'non-interactive': nonInteractive,
} = this.parseFlagsAndArgs(flags, args);
const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvPush, {
nonInteractive: false,
nonInteractive,
});
if (!environments) {
environments = await promptVariableEnvironmentAsync({
nonInteractive: false,
nonInteractive,
multiple: true,
canEnterCustomEnvironment: true,
graphqlClient,
@@ -132,6 +146,12 @@ export default class EnvPush extends EasCommand {
)} 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?`;
if (nonInteractive) {
throw new Error(
'Cannot confirm overwriting existing variables in non-interactive mode. Use --force to overwrite them.'
);
}
const confirm = await confirmAsync({
message: confirmationMessage,
});
@@ -179,6 +199,12 @@ export default class EnvPush extends EasCommand {
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
variable => `- ${variable.name}`
);
if (nonInteractive) {
throw new Error(
'Cannot confirm overwriting sensitive variables in non-interactive mode. Use --force to overwrite them.'
);
}
const confirm = await confirmAsync({
message: `You are about to overwrite sensitive variables.\n${existingSensitiveVariablesNames.join(
'\n'
@@ -212,9 +238,10 @@ export default class EnvPush extends EasCommand {
path: string;
environment: string[] | undefined;
force: boolean;
'non-interactive': boolean;
},
{ environment }: { environment?: string }
): { environment?: string[]; path: string; force: boolean } {
): { environment?: string[]; path: string; force: boolean; 'non-interactive': boolean } {
const environments = flags.environment ?? (environment ? [environment] : undefined);
return {
+25 -13
View File
@@ -13,6 +13,7 @@ import {
EASNonInteractiveFlag,
EASVariableVisibilityFlag,
EasEnvironmentFlagParameters,
validateNonInteractiveRequiredInputs,
} from '../../commandUtils/flags';
import {
EnvironmentSecretType,
@@ -63,13 +64,20 @@ interface UpdateFlags {
}
export default class EnvUpdate extends EasCommand {
static override description =
'update an environment variable on the current project or account (deprecated, use eas env:set)';
static override description = `update an environment variable on the current project or account (deprecated, use eas env:set)
In non-interactive mode, if --type is provided, --value is also required.`;
static override hidden = true;
static override examples = [
'$ eas env:update --variable-environment production --variable-name API_URL --value https://api.example.com',
'$ eas env:update --variable-environment development --variable-name GOOGLE_SERVICES_JSON --type file --value ./google-services.json --environment production',
];
static override flags = {
'variable-name': Flags.string({
description: 'Current name of the variable',
description: '(required in non-interactive mode) Current name of the variable',
}),
'variable-environment': Flags.string({
...EasEnvironmentFlagParameters,
@@ -79,7 +87,8 @@ export default class EnvUpdate extends EasCommand {
description: 'New name of the variable',
}),
value: Flags.string({
description: 'New value or the variable',
description:
'(required with --type in non-interactive mode) New value for the variable, or a file path when --type=file',
}),
type: Flags.option({
description: 'The type of variable',
@@ -217,15 +226,18 @@ export default class EnvUpdate extends EasCommand {
flags: RawUpdateFlags,
{ environment }: { environment?: string }
): UpdateFlags {
if (flags['non-interactive']) {
if (!flags['variable-name']) {
throw new Error(
'Current name is required in non-interactive mode. Run the command with --variable-name flag.'
);
}
if (flags['type'] && !flags['value']) {
throw new Error('Value is required when type is set. Run the command with --value flag.');
}
validateNonInteractiveRequiredInputs({
nonInteractive: flags['non-interactive'],
requiredInputs: [
{ name: '--variable-name', value: flags['variable-name'] },
{ if: !!flags.type, name: '--value', value: flags.value },
],
helpCommand: 'eas env:update --help',
});
if (environment && flags['variable-environment']) {
throw new Error(
"You can't use the --variable-environment flag when the environment is passed as a positional argument. Run `eas env:update --help` for more information."
);
}
const scope =