Skip to content

Commit

Permalink
Support File type environment variables (#2512)
Browse files Browse the repository at this point in the history
# Why

User should be able to upload files to environment variables and read content of those files

# How

* When an envvar is created with flag `--type FILE`, selected file is encoded to base64 and sent to www.
* EnvVarQueries return file contents when `includeFileContent` file is passed
* `env:get` automatically include file content, `env:list` requires `--include-file-content` flag
* Updated environment display - added `environments`, `type` and `visibility`

# Test Plan

tested manually, updated tests
  • Loading branch information
khamilowicz authored Oct 15, 2024
1 parent a7c77e8 commit 02022d8
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentV
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableTypeAsync,
promptVariableValueAsync,
} from '../../../utils/prompts';
import EnvironmentVariableCreate from '../create';
Expand Down Expand Up @@ -60,6 +61,7 @@ describe(EnvironmentVariableCreate, () => {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Shared,
type: EnvironmentSecretType.String,
}));
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockImplementation(async () => []);
jest.mocked(EnvironmentVariablesQuery.sharedAsync).mockImplementation(async () => []);
Expand Down Expand Up @@ -322,6 +324,10 @@ describe(EnvironmentVariableCreate, () => {

jest.mocked(promptVariableNameAsync).mockImplementation(async () => 'VarName');
jest.mocked(promptVariableValueAsync).mockImplementation(async () => 'VarValue');
jest
.mocked(promptVariableTypeAsync)
.mockImplementation(async () => EnvironmentSecretType.String);

jest
.mocked(promptVariableEnvironmentAsync)
// @ts-expect-error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Config } from '@oclif/core';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../../graphql/generated';
Expand All @@ -20,7 +22,7 @@ jest.mock('../../../utils/prompts');
describe(EnvironmentVariableGet, () => {
const graphqlClient = {} as any as ExpoGraphqlClient;
const mockConfig = {} as unknown as Config;
const mockVariables = [
const mockVariables: EnvironmentVariableFragment[] = [
{
id: 'var1',
name: 'TEST_VAR_1',
Expand All @@ -30,6 +32,7 @@ describe(EnvironmentVariableGet, () => {
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
];

Expand All @@ -55,6 +58,7 @@ describe(EnvironmentVariableGet, () => {
appId: testProjectId,
environment: undefined,
filterNames: ['TEST_VAR_1'],
includeFileContent: true,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
Expand Down Expand Up @@ -99,6 +103,7 @@ describe(EnvironmentVariableGet, () => {
updatedAt: new Date().toISOString(),
scope: EnvironmentVariableScope.Project,
visibility: EnvironmentVariableVisibility.Public,
type: EnvironmentSecretType.String,
},
]);
await command.runAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getMockAppFragment } from '../../../__tests__/commands/utils';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
import {
EnvironmentSecretType,
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
Expand All @@ -28,6 +29,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
type: EnvironmentSecretType.String,
},
{
id: 'var2',
Expand All @@ -38,6 +40,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
visibility: EnvironmentVariableVisibility.Public,
createdAt: undefined,
updatedAt: undefined,
type: EnvironmentSecretType.String,
},
];

Expand Down Expand Up @@ -65,6 +68,7 @@ describe(EnvironmentVariableList, () => {
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'));
Expand All @@ -85,6 +89,7 @@ describe(EnvironmentVariableList, () => {
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
appId: testProjectId,
environment: EnvironmentVariableEnvironment.Production,
includeFileContent: false,
});
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
Expand All @@ -109,6 +114,7 @@ describe(EnvironmentVariableList, () => {
{
appId: testProjectId,
environment: undefined,
includeFileContent: false,
}
);
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
Expand All @@ -130,6 +136,7 @@ describe(EnvironmentVariableList, () => {
expect(EnvironmentVariablesQuery.sharedAsync).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'));
Expand All @@ -155,6 +162,7 @@ describe(EnvironmentVariableList, () => {
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).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'));
Expand Down
49 changes: 44 additions & 5 deletions packages/eas-cli/src/commands/env/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';

import EasCommand from '../../commandUtils/EasCommand';
import {
Expand All @@ -25,6 +27,7 @@ import { confirmAsync } from '../../prompts';
import {
promptVariableEnvironmentAsync,
promptVariableNameAsync,
promptVariableTypeAsync,
promptVariableValueAsync,
} from '../../utils/prompts';
import { performForEnvironmentsAsync } from '../../utils/variableUtils';
Expand All @@ -34,6 +37,7 @@ type CreateFlags = {
value?: string;
link?: boolean;
force?: boolean;
type?: 'string' | 'file';
visibility?: EnvironmentVariableVisibility;
scope?: EnvironmentVariableScope;
environment?: EnvironmentVariableEnvironment[];
Expand All @@ -60,6 +64,10 @@ export default class EnvironmentVariableCreate extends EasCommand {
description: 'Overwrite existing variable',
default: false,
}),
type: Flags.enum<'string' | 'file'>({
description: 'The type of variable',
options: ['string', 'file'],
}),
...EASVariableVisibilityFlag,
...EASVariableScopeFlag,
...EASMultiEnvironmentFlag,
Expand All @@ -86,6 +94,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
visibility,
link,
force,
type,
} = await this.promptForMissingFlagsAsync(validatedFlags);

const {
Expand Down Expand Up @@ -155,6 +164,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
value,
visibility,
environments,
type,
})
: await EnvironmentVariableMutation.createForAppAsync(
graphqlClient,
Expand All @@ -163,7 +173,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
value,
environments,
visibility,
type: EnvironmentSecretType.String,
type: type ?? EnvironmentSecretType.String,
},
projectId
);
Expand Down Expand Up @@ -206,6 +216,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
value,
visibility,
environments,
type,
})
: await EnvironmentVariableMutation.createSharedVariableAsync(
graphqlClient,
Expand All @@ -214,7 +225,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
value,
visibility,
environments,
type: EnvironmentSecretType.String,
type: type ?? EnvironmentSecretType.String,
},
ownerAccount.id
);
Expand Down Expand Up @@ -283,23 +294,48 @@ export default class EnvironmentVariableCreate extends EasCommand {
environment,
visibility = EnvironmentVariableVisibility.Public,
'non-interactive': nonInteractive,
type,
...rest
}: CreateFlags): Promise<Required<CreateFlags>> {
}: CreateFlags): Promise<
Required<Omit<CreateFlags, 'type'> & { type: EnvironmentSecretType | undefined }>
> {
if (!name) {
name = await promptVariableNameAsync(nonInteractive);
}

let newType;

if (type === 'file') {
newType = EnvironmentSecretType.FileBase64;
} else if (type === 'string') {
newType = EnvironmentSecretType.String;
}

if (!type && !value && !nonInteractive) {
newType = await promptVariableTypeAsync(nonInteractive);
}

if (!value) {
value = await promptVariableValueAsync({
nonInteractive,
hidden: visibility !== EnvironmentVariableVisibility.Public,
});
}

let environmentFilePath: string | undefined;

if (newType === EnvironmentSecretType.FileBase64) {
environmentFilePath = path.resolve(value);
if (!(await fs.pathExists(environmentFilePath))) {
throw new Error(`File "${value}" does not exist`);
}
}

value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;

if (!environment) {
environment = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
}

return {
name,
value,
Expand All @@ -309,16 +345,18 @@ export default class EnvironmentVariableCreate extends EasCommand {
force: rest.force ?? false,
scope: rest.scope ?? EnvironmentVariableScope.Project,
'non-interactive': nonInteractive,
type: newType,
...rest,
};
}

private validateFlags(flags: CreateFlags): CreateFlags {
private validateFlags(flags: CreateFlags & { type?: string }): CreateFlags {
if (flags.scope !== EnvironmentVariableScope.Shared && flags.link) {
throw new Error(
`Unexpected argument: --link can only be used when creating shared variables`
);
}

if (
flags.scope === EnvironmentVariableScope.Shared &&
flags.environment &&
Expand All @@ -329,6 +367,7 @@ export default class EnvironmentVariableCreate extends EasCommand {
'Unexpected argument: --environment in non-interactive mode can only be used with --link flag.'
);
}

return flags;
}
}
9 changes: 6 additions & 3 deletions packages/eas-cli/src/commands/env/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
EnvironmentVariableEnvironment,
EnvironmentVariableFragment,
EnvironmentVariableScope,
EnvironmentVariableVisibility,
} from '../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../log';
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
import { formatVariable } from '../../utils/variableUtils';
import { formatVariable, formatVariableValue } from '../../utils/variableUtils';

type GetFlags = {
'variable-name'?: string;
Expand Down Expand Up @@ -108,7 +109,7 @@ export default class EnvironmentVariableGet extends EasCommand {
variable = variables[0];
}

if (!variable.value) {
if (variable.visibility === EnvironmentVariableVisibility.Secret) {
throw new Error(
`${chalk.bold(
variable.name
Expand All @@ -117,7 +118,7 @@ export default class EnvironmentVariableGet extends EasCommand {
}

if (format === 'short') {
Log.log(`${chalk.bold(variable.name)}=${variable.value}`);
Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`);
} else {
Log.log(formatVariable(variable));
}
Expand Down Expand Up @@ -156,6 +157,7 @@ async function getVariablesAsync(
appId: projectId,
environment,
filterNames: [name],
includeFileContent: true,
});
return appVariables;
} else {
Expand All @@ -164,6 +166,7 @@ async function getVariablesAsync(
{
appId: projectId,
filterNames: [name],
includeFileContent: true,
}
);
return sharedVariables;
Expand Down
Loading

0 comments on commit 02022d8

Please sign in to comment.