Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-14204][eas-cli] UX improvements for env:list command #2697

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions packages/eas-cli/src/commandUtils/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ export const EasNonInteractiveAndJsonFlags = {
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
required: false,
hidden: true,
};

export const EASEnvironmentFlag = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,13 @@ describe(EnvironmentVariableList, () => {
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
});

it('lists project environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);

const command = new EnvironmentVariableList([], 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'));
});

it('lists project environment variables in specified environments', async () => {
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);

const command = new EnvironmentVariableList(['--environment', 'production'], mockConfig);
const command = new EnvironmentVariableList(
['--environment', 'production', '--non-interactive'],
mockConfig
);

// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
Expand All @@ -100,7 +82,10 @@ describe(EnvironmentVariableList, () => {
.mocked(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync)
.mockResolvedValueOnce(mockVariables);

const command = new EnvironmentVariableList(['--include-sensitive'], mockConfig);
const command = new EnvironmentVariableList(
['--include-sensitive', '--environment', 'production', '--non-interactive'],
mockConfig
);

// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
Expand All @@ -113,7 +98,7 @@ describe(EnvironmentVariableList, () => {
graphqlClient,
{
appId: testProjectId,
environment: undefined,
environment: EnvironmentVariableEnvironment.Production,
includeFileContent: false,
}
);
Expand All @@ -124,7 +109,10 @@ describe(EnvironmentVariableList, () => {
it('lists shared environment variables successfully', async () => {
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockResolvedValueOnce(mockVariables);

const command = new EnvironmentVariableList(['--scope', 'shared'], mockConfig);
const command = new EnvironmentVariableList(
['--scope', 'shared', '--non-interactive', '--environment', 'production'],
mockConfig
);

// @ts-expect-error
jest.spyOn(command, 'getContextAsync').mockReturnValue({
Expand All @@ -135,7 +123,7 @@ describe(EnvironmentVariableList, () => {

expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: EnvironmentVariableEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
Expand All @@ -148,7 +136,14 @@ describe(EnvironmentVariableList, () => {
.mockResolvedValueOnce(mockVariables);

const command = new EnvironmentVariableList(
['--include-sensitive', '--scope', 'shared'],
[
'--include-sensitive',
'--scope',
'shared',
'--non-interactive',
'--environment',
'production',
],
mockConfig
);

Expand All @@ -161,7 +156,7 @@ describe(EnvironmentVariableList, () => {

expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: undefined,
environment: EnvironmentVariableEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
Expand Down
134 changes: 67 additions & 67 deletions packages/eas-cli/src/commands/env/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import {
EASMultiEnvironmentFlag,
EASEnvironmentFlag,
EASNonInteractiveFlag,
EASVariableFormatFlag,
EASVariableScopeFlag,
} from '../../commandUtils/flags';
Expand All @@ -15,12 +16,7 @@ import {
} from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
import {
formatVariable,
formatVariableValue,
isEnvironment,
performForEnvironmentsAsync,
} from '../../utils/variableUtils';
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';

async function getVariablesForScopeAsync(
graphqlClient: ExpoGraphqlClient,
Expand Down Expand Up @@ -66,16 +62,25 @@ async function getVariablesForScopeAsync(
});
}

type ListFlags = {
interface RawListFlags {
scope: EnvironmentVariableScope;
format: string;
environment: EnvironmentVariableEnvironment[] | undefined;
environment?: EnvironmentVariableEnvironment;
'include-sensitive': boolean;
'include-file-content': boolean;
'non-interactive'?: boolean;
};
'non-interactive': boolean;
}

interface ListArgs {
scope: EnvironmentVariableScope;
format: string;
environment: EnvironmentVariableEnvironment;
includeSensitive: boolean;
includeFileContent: boolean;
nonInteractive: boolean;
}

export default class EnvironmentValueList extends EasCommand {
export default class EnvList extends EasCommand {
static override description = 'list environment variables for the current project';

static override hidden = true;
Expand All @@ -94,9 +99,10 @@ export default class EnvironmentValueList extends EasCommand {
description: 'Display files content in the output',
default: false,
}),
...EASMultiEnvironmentFlag,
...EASEnvironmentFlag,
...EASVariableFormatFlag,
...EASVariableScopeFlag,
...EASNonInteractiveFlag,
};

static override args = [
Expand All @@ -109,82 +115,76 @@ export default class EnvironmentValueList extends EasCommand {
];

async runAsync(): Promise<void> {
const { args, flags } = await this.parse(EnvironmentValueList);
const { args, flags } = await this.parse(EnvList);

let {
format,
environment: environments,
scope,
'include-sensitive': includeSensitive,
'include-file-content': includeFileContent,
'non-interactive': nonInteractive,
} = this.validateInputs(flags, args);
const { environment, format, scope, includeFileContent, includeSensitive, nonInteractive } =
await this.sanitizeInputsAsync(flags, args);

const {
projectId,
loggedIn: { graphqlClient },
} = await this.getContextAsync(EnvironmentValueList, {
nonInteractive: true,
} = await this.getContextAsync(EnvList, {
nonInteractive,
});

if (!environments) {
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}
const variables = await getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
includeFileContent,
environment,
projectId,
});

await performForEnvironmentsAsync(environments, async environment => {
const variables = await getVariablesForScopeAsync(graphqlClient, {
scope,
includingSensitive: includeSensitive,
includeFileContent,
environment,
projectId,
});
Log.addNewLineIfNone();
if (environment) {
Log.log(chalk.bold(`Environment: ${environment.toLocaleLowerCase()}`));
}

Log.addNewLineIfNone();
if (environment) {
Log.log(chalk.bold(`Environment: ${environment.toLocaleLowerCase()}`));
}
if (variables.length === 0) {
Log.log('No variables found for this environment.');
return;
}

if (variables.length === 0) {
Log.log('No variables found for this environment.');
return;
if (format === 'short') {
for (const variable of variables) {
Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`);
}

if (format === 'short') {
for (const variable of variables) {
Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`);
}
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
if (scope === EnvironmentVariableScope.Shared) {
Log.log(chalk.bold('Shared variables for this account:'));
} else {
Log.log(chalk.bold(`Variables for this project:`));
}
Log.log(
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
Log.log(chalk.bold(`Variables for this project:`));
}
});
Log.log(
variables.map(variable => formatVariable(variable)).join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}

private validateInputs(
flags: ListFlags,
{ environment }: Record<string, string>
): ListFlags & { 'non-interactive': boolean } {
if (environment && !isEnvironment(environment.toUpperCase())) {
private async sanitizeInputsAsync(
flags: RawListFlags,
{ environment: environmentInput }: Record<string, string>
): Promise<ListArgs> {
if (environmentInput && !isEnvironment(environmentInput.toUpperCase())) {
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
}

const environments = flags.environment
const environmentFromNonInteractiveInputs = flags.environment
? flags.environment
: environment
? [environment.toUpperCase() as EnvironmentVariableEnvironment]
: undefined;
: environmentInput
? (environmentInput.toUpperCase() as EnvironmentVariableEnvironment)
: null;

const environment = environmentFromNonInteractiveInputs
? environmentFromNonInteractiveInputs
: await promptVariableEnvironmentAsync({ nonInteractive: flags['non-interactive'] });

return {
...flags,
'non-interactive': flags['non-interactive'] ?? false,
environment: environments,
nonInteractive: flags['non-interactive'],
includeFileContent: flags['include-file-content'],
includeSensitive: flags['include-sensitive'],
environment,
};
}
}
31 changes: 22 additions & 9 deletions packages/eas-cli/src/utils/variableUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ export function formatVariableValue(variable: EnvironmentVariableWithFileContent
}

if (variable.visibility === EnvironmentVariableVisibility.Sensitive) {
return '***** (This is a sensitive env variable. To access it, run command with --include-sensitive flag. Learn more.)';
return '*****(Sensitive)';
}

if (variable.visibility === EnvironmentVariableVisibility.Secret) {
return "***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)";
return '*****(Secret)';
}

if (variable.type === EnvironmentSecretType.FileBase64) {
return '***** (This is a file env variable. To access it, run command with --include-file-content flag. Learn more.)';
return '(File type variable)';
}

return '*****';
Expand All @@ -62,20 +62,33 @@ export async function performForEnvironmentsAsync(

export function formatVariable(variable: EnvironmentVariableWithFileContent): string {
return formatFields([
{ label: 'ID', value: variable.id },
{ label: 'Name', value: variable.name },
{ label: 'Value', value: formatVariableValue(variable) },
{ label: 'Scope', value: variable.scope },
{ label: 'Visibility', value: variable.visibility ?? '' },
{
label: 'Scope',
value: variable.scope === EnvironmentVariableScope.Project ? 'Project' : 'Account',
},
{
label: 'Visibility',
value: variable.visibility ? visibilityToVisibilityName[variable.visibility] : '',
},
{
label: 'Environments',
value: variable.environments ? variable.environments.join(', ') : '-',
value: variable.environments
? variable.environments.map(env => env.toLowerCase()).join(', ')
: '-',
},
{
label: 'type',
value: variable.type === EnvironmentSecretType.FileBase64 ? 'file' : 'string',
label: 'Type',
value: variable.type === EnvironmentSecretType.FileBase64 ? 'File' : 'String',
},
{ label: 'Created at', value: dateFormat(variable.createdAt, 'mmm dd HH:MM:ss') },
{ label: 'Updated at', value: dateFormat(variable.updatedAt, 'mmm dd HH:MM:ss') },
]);
}

const visibilityToVisibilityName: Record<EnvironmentVariableVisibility, string> = {
[EnvironmentVariableVisibility.Public]: 'Plain text',
[EnvironmentVariableVisibility.Sensitive]: 'Sensitive',
[EnvironmentVariableVisibility.Secret]: 'Secret',
};
Loading