From 62f3817554c995249f5e3996042031f53d900bb1 Mon Sep 17 00:00:00 2001 From: Piotr Szeremeta Date: Wed, 21 Aug 2024 14:25:16 +0200 Subject: [PATCH] Support File type environment variables --- packages/eas-cli/README.md | 140 ++++++++-------- .../EnvironmentVariableCreate.test.ts | 6 + .../__tests__/EnvironmentVariableGet.test.ts | 7 +- .../__tests__/EnvironmentVariableList.test.ts | 8 + packages/eas-cli/src/commands/env/create.ts | 49 +++++- packages/eas-cli/src/commands/env/get.ts | 9 +- packages/eas-cli/src/commands/env/list.ts | 42 +++-- packages/eas-cli/src/commands/env/update.ts | 151 +++++++++++++++--- packages/eas-cli/src/graphql/generated.ts | 24 +-- .../queries/EnvironmentVariablesQuery.ts | 63 ++++++-- .../src/graphql/types/EnvironmentVariable.ts | 1 + .../types/EnvironmentVariableWithSecret.ts | 15 ++ packages/eas-cli/src/prompts.ts | 3 + packages/eas-cli/src/utils/prompts.ts | 25 +++ packages/eas-cli/src/utils/variableUtils.ts | 46 +++++- 15 files changed, 441 insertions(+), 148 deletions(-) create mode 100644 packages/eas-cli/src/graphql/types/EnvironmentVariableWithSecret.ts diff --git a/packages/eas-cli/README.md b/packages/eas-cli/README.md index 99aea9a886..02b7bbc372 100644 --- a/packages/eas-cli/README.md +++ b/packages/eas-cli/README.md @@ -55,76 +55,75 @@ eas --help COMMAND # Commands - -- [`eas account:login`](#eas-accountlogin) -- [`eas account:logout`](#eas-accountlogout) -- [`eas account:view`](#eas-accountview) -- [`eas analytics [STATUS]`](#eas-analytics-status) -- [`eas autocomplete [SHELL]`](#eas-autocomplete-shell) -- [`eas branch:create [NAME]`](#eas-branchcreate-name) -- [`eas branch:delete [NAME]`](#eas-branchdelete-name) -- [`eas branch:list`](#eas-branchlist) -- [`eas branch:rename`](#eas-branchrename) -- [`eas branch:view [NAME]`](#eas-branchview-name) -- [`eas build`](#eas-build) -- [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id) -- [`eas build:configure`](#eas-buildconfigure) -- [`eas build:delete [BUILD_ID]`](#eas-builddelete-build_id) -- [`eas build:inspect`](#eas-buildinspect) -- [`eas build:list`](#eas-buildlist) -- [`eas build:resign`](#eas-buildresign) -- [`eas build:run`](#eas-buildrun) -- [`eas build:submit`](#eas-buildsubmit) -- [`eas build:version:get`](#eas-buildversionget) -- [`eas build:version:set`](#eas-buildversionset) -- [`eas build:version:sync`](#eas-buildversionsync) -- [`eas build:view [BUILD_ID]`](#eas-buildview-build_id) -- [`eas channel:create [NAME]`](#eas-channelcreate-name) -- [`eas channel:edit [NAME]`](#eas-channeledit-name) -- [`eas channel:list`](#eas-channellist) -- [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel) -- [`eas channel:view [NAME]`](#eas-channelview-name) -- [`eas config`](#eas-config) -- [`eas credentials`](#eas-credentials) -- [`eas credentials:configure-build`](#eas-credentialsconfigure-build) -- [`eas device:create`](#eas-devicecreate) -- [`eas device:delete`](#eas-devicedelete) -- [`eas device:list`](#eas-devicelist) -- [`eas device:rename`](#eas-devicerename) -- [`eas device:view [UDID]`](#eas-deviceview-udid) -- [`eas diagnostics`](#eas-diagnostics) -- [`eas help [COMMAND]`](#eas-help-command) -- [`eas init`](#eas-init) -- [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory) -- [`eas login`](#eas-login) -- [`eas logout`](#eas-logout) -- [`eas metadata:lint`](#eas-metadatalint) -- [`eas metadata:pull`](#eas-metadatapull) -- [`eas metadata:push`](#eas-metadatapush) -- [`eas onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-onboarding-target_project_directory) -- [`eas open`](#eas-open) -- [`eas project:info`](#eas-projectinfo) -- [`eas project:init`](#eas-projectinit) -- [`eas project:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-projectonboarding-target_project_directory) -- [`eas secret:create`](#eas-secretcreate) -- [`eas secret:delete`](#eas-secretdelete) -- [`eas secret:list`](#eas-secretlist) -- [`eas secret:push`](#eas-secretpush) -- [`eas submit`](#eas-submit) -- [`eas update`](#eas-update) -- [`eas update:configure`](#eas-updateconfigure) -- [`eas update:delete GROUPID`](#eas-updatedelete-groupid) -- [`eas update:list`](#eas-updatelist) -- [`eas update:republish`](#eas-updaterepublish) -- [`eas update:roll-back-to-embedded`](#eas-updateroll-back-to-embedded) -- [`eas update:rollback`](#eas-updaterollback) -- [`eas update:view GROUPID`](#eas-updateview-groupid) -- [`eas webhook:create`](#eas-webhookcreate) -- [`eas webhook:delete [ID]`](#eas-webhookdelete-id) -- [`eas webhook:list`](#eas-webhooklist) -- [`eas webhook:update`](#eas-webhookupdate) -- [`eas webhook:view ID`](#eas-webhookview-id) -- [`eas whoami`](#eas-whoami) +* [`eas account:login`](#eas-accountlogin) +* [`eas account:logout`](#eas-accountlogout) +* [`eas account:view`](#eas-accountview) +* [`eas analytics [STATUS]`](#eas-analytics-status) +* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell) +* [`eas branch:create [NAME]`](#eas-branchcreate-name) +* [`eas branch:delete [NAME]`](#eas-branchdelete-name) +* [`eas branch:list`](#eas-branchlist) +* [`eas branch:rename`](#eas-branchrename) +* [`eas branch:view [NAME]`](#eas-branchview-name) +* [`eas build`](#eas-build) +* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id) +* [`eas build:configure`](#eas-buildconfigure) +* [`eas build:delete [BUILD_ID]`](#eas-builddelete-build_id) +* [`eas build:inspect`](#eas-buildinspect) +* [`eas build:list`](#eas-buildlist) +* [`eas build:resign`](#eas-buildresign) +* [`eas build:run`](#eas-buildrun) +* [`eas build:submit`](#eas-buildsubmit) +* [`eas build:version:get`](#eas-buildversionget) +* [`eas build:version:set`](#eas-buildversionset) +* [`eas build:version:sync`](#eas-buildversionsync) +* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id) +* [`eas channel:create [NAME]`](#eas-channelcreate-name) +* [`eas channel:edit [NAME]`](#eas-channeledit-name) +* [`eas channel:list`](#eas-channellist) +* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel) +* [`eas channel:view [NAME]`](#eas-channelview-name) +* [`eas config`](#eas-config) +* [`eas credentials`](#eas-credentials) +* [`eas credentials:configure-build`](#eas-credentialsconfigure-build) +* [`eas device:create`](#eas-devicecreate) +* [`eas device:delete`](#eas-devicedelete) +* [`eas device:list`](#eas-devicelist) +* [`eas device:rename`](#eas-devicerename) +* [`eas device:view [UDID]`](#eas-deviceview-udid) +* [`eas diagnostics`](#eas-diagnostics) +* [`eas help [COMMAND]`](#eas-help-command) +* [`eas init`](#eas-init) +* [`eas init:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-initonboarding-target_project_directory) +* [`eas login`](#eas-login) +* [`eas logout`](#eas-logout) +* [`eas metadata:lint`](#eas-metadatalint) +* [`eas metadata:pull`](#eas-metadatapull) +* [`eas metadata:push`](#eas-metadatapush) +* [`eas onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-onboarding-target_project_directory) +* [`eas open`](#eas-open) +* [`eas project:info`](#eas-projectinfo) +* [`eas project:init`](#eas-projectinit) +* [`eas project:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-projectonboarding-target_project_directory) +* [`eas secret:create`](#eas-secretcreate) +* [`eas secret:delete`](#eas-secretdelete) +* [`eas secret:list`](#eas-secretlist) +* [`eas secret:push`](#eas-secretpush) +* [`eas submit`](#eas-submit) +* [`eas update`](#eas-update) +* [`eas update:configure`](#eas-updateconfigure) +* [`eas update:delete GROUPID`](#eas-updatedelete-groupid) +* [`eas update:list`](#eas-updatelist) +* [`eas update:republish`](#eas-updaterepublish) +* [`eas update:roll-back-to-embedded`](#eas-updateroll-back-to-embedded) +* [`eas update:rollback`](#eas-updaterollback) +* [`eas update:view GROUPID`](#eas-updateview-groupid) +* [`eas webhook:create`](#eas-webhookcreate) +* [`eas webhook:delete [ID]`](#eas-webhookdelete-id) +* [`eas webhook:list`](#eas-webhooklist) +* [`eas webhook:update`](#eas-webhookupdate) +* [`eas webhook:view ID`](#eas-webhookview-id) +* [`eas whoami`](#eas-whoami) ## `eas account:login` @@ -1586,5 +1585,4 @@ DESCRIPTION ALIASES $ eas whoami ``` - diff --git a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableCreate.test.ts b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableCreate.test.ts index 060857811a..7b5062edea 100644 --- a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableCreate.test.ts +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableCreate.test.ts @@ -15,6 +15,7 @@ import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentV import { promptVariableEnvironmentAsync, promptVariableNameAsync, + promptVariableTypeAsync, promptVariableValueAsync, } from '../../../utils/prompts'; import EnvironmentVariableCreate from '../create'; @@ -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 () => []); @@ -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 diff --git a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableGet.test.ts b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableGet.test.ts index 4d8bebd6b7..51b469996e 100644 --- a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableGet.test.ts +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableGet.test.ts @@ -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'; @@ -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', @@ -30,6 +32,7 @@ describe(EnvironmentVariableGet, () => { updatedAt: new Date().toISOString(), scope: EnvironmentVariableScope.Project, visibility: EnvironmentVariableVisibility.Public, + type: EnvironmentSecretType.String, }, ]; @@ -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')); @@ -99,6 +103,7 @@ describe(EnvironmentVariableGet, () => { updatedAt: new Date().toISOString(), scope: EnvironmentVariableScope.Project, visibility: EnvironmentVariableVisibility.Public, + type: EnvironmentSecretType.String, }, ]); await command.runAsync(); diff --git a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts index 4b69d5ddf2..9684d55c42 100644 --- a/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts +++ b/packages/eas-cli/src/commands/env/__tests__/EnvironmentVariableList.test.ts @@ -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, @@ -28,6 +29,7 @@ const mockVariables: EnvironmentVariableFragment[] = [ visibility: EnvironmentVariableVisibility.Public, createdAt: undefined, updatedAt: undefined, + type: EnvironmentSecretType.String, }, { id: 'var2', @@ -38,6 +40,7 @@ const mockVariables: EnvironmentVariableFragment[] = [ visibility: EnvironmentVariableVisibility.Public, createdAt: undefined, updatedAt: undefined, + type: EnvironmentSecretType.String, }, ]; @@ -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')); @@ -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')); @@ -109,6 +114,7 @@ describe(EnvironmentVariableList, () => { { appId: testProjectId, environment: undefined, + includeFileContent: false, } ); expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1')); @@ -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')); @@ -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')); diff --git a/packages/eas-cli/src/commands/env/create.ts b/packages/eas-cli/src/commands/env/create.ts index cf45de2c34..5e8d573c47 100644 --- a/packages/eas-cli/src/commands/env/create.ts +++ b/packages/eas-cli/src/commands/env/create.ts @@ -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 { @@ -25,6 +27,7 @@ import { confirmAsync } from '../../prompts'; import { promptVariableEnvironmentAsync, promptVariableNameAsync, + promptVariableTypeAsync, promptVariableValueAsync, } from '../../utils/prompts'; import { performForEnvironmentsAsync } from '../../utils/variableUtils'; @@ -34,6 +37,7 @@ type CreateFlags = { value?: string; link?: boolean; force?: boolean; + type?: 'string' | 'file'; visibility?: EnvironmentVariableVisibility; scope?: EnvironmentVariableScope; environment?: EnvironmentVariableEnvironment[]; @@ -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, @@ -86,6 +94,7 @@ export default class EnvironmentVariableCreate extends EasCommand { visibility, link, force, + type, } = await this.promptForMissingFlagsAsync(validatedFlags); const { @@ -155,6 +164,7 @@ export default class EnvironmentVariableCreate extends EasCommand { value, visibility, environments, + type, }) : await EnvironmentVariableMutation.createForAppAsync( graphqlClient, @@ -163,7 +173,7 @@ export default class EnvironmentVariableCreate extends EasCommand { value, environments, visibility, - type: EnvironmentSecretType.String, + type: type ?? EnvironmentSecretType.String, }, projectId ); @@ -206,6 +216,7 @@ export default class EnvironmentVariableCreate extends EasCommand { value, visibility, environments, + type, }) : await EnvironmentVariableMutation.createSharedVariableAsync( graphqlClient, @@ -214,7 +225,7 @@ export default class EnvironmentVariableCreate extends EasCommand { value, visibility, environments, - type: EnvironmentSecretType.String, + type: type ?? EnvironmentSecretType.String, }, ownerAccount.id ); @@ -283,12 +294,27 @@ export default class EnvironmentVariableCreate extends EasCommand { environment, visibility = EnvironmentVariableVisibility.Public, 'non-interactive': nonInteractive, + type, ...rest - }: CreateFlags): Promise> { + }: CreateFlags): Promise< + Required & { 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, @@ -296,10 +322,20 @@ export default class EnvironmentVariableCreate extends EasCommand { }); } + 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, @@ -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 && @@ -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; } } diff --git a/packages/eas-cli/src/commands/env/get.ts b/packages/eas-cli/src/commands/env/get.ts index 15d0a5eb70..b436d7244d 100644 --- a/packages/eas-cli/src/commands/env/get.ts +++ b/packages/eas-cli/src/commands/env/get.ts @@ -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; @@ -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 @@ -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)); } @@ -156,6 +157,7 @@ async function getVariablesAsync( appId: projectId, environment, filterNames: [name], + includeFileContent: true, }); return appVariables; } else { @@ -164,6 +166,7 @@ async function getVariablesAsync( { appId: projectId, filterNames: [name], + includeFileContent: true, } ); return sharedVariables; diff --git a/packages/eas-cli/src/commands/env/list.ts b/packages/eas-cli/src/commands/env/list.ts index 4cbfede2f8..56775d5670 100644 --- a/packages/eas-cli/src/commands/env/list.ts +++ b/packages/eas-cli/src/commands/env/list.ts @@ -8,40 +8,47 @@ import { EASVariableFormatFlag, EASVariableScopeFlag, } from '../../commandUtils/flags'; +import { EnvironmentVariableEnvironment, EnvironmentVariableScope } from '../../graphql/generated'; import { - EnvironmentVariableEnvironment, - EnvironmentVariableFragment, - EnvironmentVariableScope, -} from '../../graphql/generated'; -import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery'; + EnvironmentVariableWithFileContent, + EnvironmentVariablesQuery, +} from '../../graphql/queries/EnvironmentVariablesQuery'; import Log from '../../log'; import { promptVariableEnvironmentAsync } from '../../utils/prompts'; -import { formatVariable, performForEnvironmentsAsync } from '../../utils/variableUtils'; +import { + formatVariable, + formatVariableValue, + performForEnvironmentsAsync, +} from '../../utils/variableUtils'; async function getVariablesForScopeAsync( graphqlClient: ExpoGraphqlClient, { scope, includingSensitive, + includeFileContent, environment, projectId, }: { scope: EnvironmentVariableScope; includingSensitive: boolean; + includeFileContent: boolean; environment?: EnvironmentVariableEnvironment; projectId: string; } -): Promise { +): Promise { if (scope === EnvironmentVariableScope.Project) { if (includingSensitive) { return await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(graphqlClient, { appId: projectId, environment, + includeFileContent, }); } return await EnvironmentVariablesQuery.byAppIdAsync(graphqlClient, { appId: projectId, environment, + includeFileContent, }); } @@ -49,8 +56,13 @@ async function getVariablesForScopeAsync( ? await EnvironmentVariablesQuery.sharedWithSensitiveAsync(graphqlClient, { appId: projectId, environment, + includeFileContent, }) - : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { appId: projectId, environment }); + : await EnvironmentVariablesQuery.sharedAsync(graphqlClient, { + appId: projectId, + environment, + includeFileContent, + }); } export default class EnvironmentValueList extends EasCommand { @@ -68,6 +80,10 @@ export default class EnvironmentValueList extends EasCommand { description: 'Display sensitive values in the output', default: false, }), + 'include-file-content': Flags.boolean({ + description: 'Display files content in the output', + default: false, + }), ...EASVariableFormatFlag, ...EASVariableScopeFlag, ...EASMultiEnvironmentFlag, @@ -80,6 +96,7 @@ export default class EnvironmentValueList extends EasCommand { format, scope, 'include-sensitive': includeSensitive, + 'include-file-content': includeFileContent, 'non-interactive': nonInteractive, }, } = await this.parse(EnvironmentValueList); @@ -98,6 +115,7 @@ export default class EnvironmentValueList extends EasCommand { const variables = await getVariablesForScopeAsync(graphqlClient, { scope, includingSensitive: includeSensitive, + includeFileContent, environment, projectId, }); @@ -109,13 +127,7 @@ export default class EnvironmentValueList extends EasCommand { if (format === 'short') { for (const variable of variables) { - // TODO: Add Learn more link - Log.log( - `${chalk.bold(variable.name)}=${ - variable.value ?? - "***** (This is a secret env variable that can only be accessed on EAS builder and can't be read in any UI. Learn more.)" - }` - ); + Log.log(`${chalk.bold(variable.name)}=${formatVariableValue(variable)}`); } } else { if (scope === EnvironmentVariableScope.Shared) { diff --git a/packages/eas-cli/src/commands/env/update.ts b/packages/eas-cli/src/commands/env/update.ts index 0a6fd46eda..b32c317164 100644 --- a/packages/eas-cli/src/commands/env/update.ts +++ b/packages/eas-cli/src/commands/env/update.ts @@ -1,6 +1,8 @@ import { Flags } from '@oclif/core'; import assert from 'assert'; import chalk from 'chalk'; +import fs from 'fs-extra'; +import path from 'path'; import EasCommand from '../../commandUtils/EasCommand'; import { @@ -11,6 +13,7 @@ import { EasEnvironmentFlagParameters, } from '../../commandUtils/flags'; import { + EnvironmentSecretType, EnvironmentVariableEnvironment, EnvironmentVariableFragment, EnvironmentVariableScope, @@ -27,6 +30,7 @@ import { selectAsync } from '../../prompts'; import { promptVariableEnvironmentAsync, promptVariableNameAsync, + promptVariableTypeAsync, promptVariableValueAsync, promptVariableVisibilityAsync, } from '../../utils/prompts'; @@ -38,6 +42,7 @@ type UpdateFlags = { scope?: EnvironmentVariableScope; environment?: EnvironmentVariableEnvironment[]; visibility?: EnvironmentVariableVisibility; + type?: 'string' | 'file'; 'variable-name'?: string; 'variable-environment'?: EnvironmentVariableEnvironment; 'non-interactive': boolean; @@ -63,6 +68,10 @@ export default class EnvironmentVariableUpdate extends EasCommand { value: Flags.string({ description: 'New value or the variable', }), + type: Flags.enum<'string' | 'file'>({ + description: 'The type of variable', + options: ['string', 'file'], + }), ...EASVariableVisibilityFlag, ...EASVariableScopeFlag, ...EASMultiEnvironmentFlag, @@ -77,14 +86,15 @@ export default class EnvironmentVariableUpdate extends EasCommand { async runAsync(): Promise { const { flags } = await this.parse(EnvironmentVariableUpdate); - let { + const { name, - value, + value: rawValue, scope, 'variable-name': currentName, 'variable-environment': currentEnvironment, 'non-interactive': nonInteractive, environment: environments, + type, visibility, } = this.validateFlags(flags); @@ -142,31 +152,133 @@ export default class EnvironmentVariableUpdate extends EasCommand { } assert(selectedVariable, 'Variable must be selected'); + + const { + name: newName, + value: newValue, + environment: newEnvironments, + visibility: newVisibility, + type: newType, + } = await this.promptForMissingFlagsAsync(selectedVariable, { + name, + value: rawValue, + environment: environments, + visibility, + 'non-interactive': nonInteractive, + type, + }); + + const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, { + id: selectedVariable.id, + name: newName, + value: newValue, + environments: newEnvironments, + type: newType, + visibility: newVisibility, + }); + if (!variable) { + throw new Error(`Could not update variable with name ${name} ${suffix}`); + } + + Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`); + } + private validateFlags(flags: UpdateFlags): 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.'); + } + } + + return flags; + } + + private async promptForMissingFlagsAsync( + selectedVariable: EnvironmentVariableFragment, + { + name, + value, + environment: environments, + visibility, + 'non-interactive': nonInteractive, + type, + ...rest + }: UpdateFlags + ): Promise & { type?: EnvironmentSecretType }> { + let newType; + + if (type === 'file') { + newType = EnvironmentSecretType.FileBase64; + } else if (type === 'string') { + newType = EnvironmentSecretType.String; + } + if (!nonInteractive) { if (!name) { name = await promptVariableNameAsync(nonInteractive, selectedVariable.name); + if (!name || name.length === 0) { name = undefined; } } + Log.log( + selectedVariable.type, + EnvironmentSecretType.String === selectedVariable.type, + EnvironmentSecretType.FileBase64 === selectedVariable.type + ); + if (!type && !value && !nonInteractive) { + newType = await promptVariableTypeAsync(nonInteractive, selectedVariable.type); + + if (!newType || newType === selectedVariable.type) { + newType = undefined; + } + } + if (!value) { value = await promptVariableValueAsync({ nonInteractive, required: false, - initial: selectedVariable.value, + initial: + (newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64 + ? undefined + : selectedVariable.value, }); - if (!value || value.length === 0) { + + if (!value || value.length === 0 || value === selectedVariable.value) { value = undefined; } } + let environmentFilePath: string | undefined; + + if (newType === EnvironmentSecretType.FileBase64 && value) { + 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 (!environments || environments.length === 0) { environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true, selectedEnvironments: selectedVariable.environments ?? [], }); + + if ( + !environments || + environments.length === 0 || + environments === selectedVariable.environments + ) { + environments = undefined; + } } if (!visibility) { @@ -174,31 +286,22 @@ export default class EnvironmentVariableUpdate extends EasCommand { nonInteractive, selectedVariable.visibility ); + + if (!visibility || visibility === selectedVariable.visibility) { + visibility = undefined; + } } } - const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, { - id: selectedVariable.id, + return { name, value, - environments, + environment: environments, visibility, - }); - if (!variable) { - throw new Error(`Could not update variable with name ${name} ${suffix}`); - } - - Log.withTick(`Updated variable ${chalk.bold(selectedVariable.name)} ${suffix}.`); - } - private validateFlags(flags: UpdateFlags): 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.' - ); - } - } - - return flags; + scope: rest.scope ?? EnvironmentVariableScope.Project, + 'non-interactive': nonInteractive, + type: newType, + ...rest, + }; } } diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index 170b81e961..35ccfdaa62 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -8436,7 +8436,7 @@ export type LinkSharedEnvironmentVariableMutationVariables = Exact<{ }>; -export type LinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', linkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } }; +export type LinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', linkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } }; export type UnlinkSharedEnvironmentVariableMutationVariables = Exact<{ appId: Scalars['ID']['input']; @@ -8445,7 +8445,7 @@ export type UnlinkSharedEnvironmentVariableMutationVariables = Exact<{ }>; -export type UnlinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', unlinkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } }; +export type UnlinkSharedEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', unlinkSharedEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } }; export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{ input: CreateSharedEnvironmentVariableInput; @@ -8453,7 +8453,7 @@ export type CreateEnvironmentVariableForAccountMutationVariables = Exact<{ }>; -export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } }; +export type CreateEnvironmentVariableForAccountMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForAccount: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } }; export type CreateEnvironmentVariableForAppMutationVariables = Exact<{ input: CreateEnvironmentVariableInput; @@ -8461,14 +8461,14 @@ export type CreateEnvironmentVariableForAppMutationVariables = Exact<{ }>; -export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } }; +export type CreateEnvironmentVariableForAppMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', createEnvironmentVariableForApp: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } }; export type UpdateEnvironmentVariableMutationVariables = Exact<{ input: UpdateEnvironmentVariableInput; }>; -export type UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null } } }; +export type UpdateEnvironmentVariableMutation = { __typename?: 'RootMutation', environmentVariable: { __typename?: 'EnvironmentVariableMutation', updateEnvironmentVariable: { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType } } }; export type DeleteEnvironmentVariableMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -8719,37 +8719,41 @@ export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact< appId: Scalars['String']['input']; filterNames?: InputMaybe | Scalars['String']['input']>; environment?: InputMaybe; + includeFileContent: Scalars['Boolean']['input']; }>; -export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null }> } } }; +export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null, valueWithFileContent?: string | null }> } } }; export type EnvironmentVariablesByAppIdQueryVariables = Exact<{ appId: Scalars['String']['input']; filterNames?: InputMaybe | Scalars['String']['input']>; environment?: InputMaybe; + includeFileContent: Scalars['Boolean']['input']; }>; -export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array | null, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } }; +export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array | null, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } }; export type EnvironmentVariablesSharedQueryVariables = Exact<{ appId: Scalars['String']['input']; filterNames?: InputMaybe | Scalars['String']['input']>; environment?: InputMaybe; + includeFileContent: Scalars['Boolean']['input']; }>; -export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array | null, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } } }; +export type EnvironmentVariablesSharedQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, linkedEnvironments?: Array | null, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } }; export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{ appId: Scalars['String']['input']; filterNames?: InputMaybe | Scalars['String']['input']>; environment?: InputMaybe; + includeFileContent: Scalars['Boolean']['input']; }>; -export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null }> } } } }; +export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null, valueWithFileContent?: string | null }> } } } }; export type GoogleServiceAccountKeyByIdQueryVariables = Exact<{ ascApiKeyId: Scalars['ID']['input']; @@ -8868,7 +8872,7 @@ export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, s export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }; -export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }; +export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null, type: EnvironmentSecretType }; export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: string }; diff --git a/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts b/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts index 341e0d24d7..2a7a1f83ba 100644 --- a/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts +++ b/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts @@ -11,11 +11,16 @@ import { EnvironmentVariablesSharedQueryVariables, } from '../generated'; import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable'; +import { EnvironmentVariableWithSecretFragmentNode } from '../types/EnvironmentVariableWithSecret'; type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & { linkedEnvironments?: EnvironmentVariableEnvironment[] | null; }; +export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & { + valueWithFileContent?: string | null | undefined; +}; + export const EnvironmentVariablesQuery = { async byAppIdWithSensitiveAsync( graphqlClient: ExpoGraphqlClient, @@ -23,12 +28,14 @@ export const EnvironmentVariablesQuery = { appId, environment, filterNames, + includeFileContent = false, }: { appId: string; environment?: EnvironmentVariableEnvironment; filterNames?: string[]; + includeFileContent?: boolean; } - ): Promise { + ): Promise { const data = await withErrorHandlingAsync( graphqlClient .query( @@ -37,6 +44,7 @@ export const EnvironmentVariablesQuery = { $appId: String! $filterNames: [String!] $environment: EnvironmentVariableEnvironment + $includeFileContent: Boolean! ) { app { byId(appId: $appId) { @@ -46,15 +54,16 @@ export const EnvironmentVariablesQuery = { environment: $environment ) { id - name - value - environments + ...EnvironmentVariableWithSecretFragment + valueWithFileContent: value(includeFileContent: $includeFileContent) + @include(if: $includeFileContent) } } } } + ${print(EnvironmentVariableWithSecretFragmentNode)} `, - { appId, filterNames, environment }, + { appId, filterNames, environment, includeFileContent }, { additionalTypenames: ['EnvironmentVariableWithSecret'] } ) .toPromise() @@ -68,12 +77,14 @@ export const EnvironmentVariablesQuery = { appId, environment, filterNames, + includeFileContent = false, }: { appId: string; environment?: EnvironmentVariableEnvironment; filterNames?: string[]; + includeFileContent?: boolean; } - ): Promise { + ): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> { const data = await withErrorHandlingAsync( graphqlClient .query( @@ -82,6 +93,7 @@ export const EnvironmentVariablesQuery = { $appId: String! $filterNames: [String!] $environment: EnvironmentVariableEnvironment + $includeFileContent: Boolean! ) { app { byId(appId: $appId) { @@ -90,13 +102,15 @@ export const EnvironmentVariablesQuery = { id linkedEnvironments(appId: $appId) ...EnvironmentVariableFragment + valueWithFileContent: value(includeFileContent: $includeFileContent) + @include(if: $includeFileContent) } } } } ${print(EnvironmentVariableFragmentNode)} `, - { appId, filterNames, environment }, + { appId, filterNames, environment, includeFileContent }, { additionalTypenames: ['EnvironmentVariable'] } ) .toPromise() @@ -110,8 +124,14 @@ export const EnvironmentVariablesQuery = { appId, filterNames, environment, - }: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment } - ): Promise { + includeFileContent = false, + }: { + appId: string; + filterNames?: string[]; + environment?: EnvironmentVariableEnvironment; + includeFileContent?: boolean; + } + ): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> { const data = await withErrorHandlingAsync( graphqlClient .query( @@ -120,6 +140,7 @@ export const EnvironmentVariablesQuery = { $appId: String! $filterNames: [String!] $environment: EnvironmentVariableEnvironment + $includeFileContent: Boolean! ) { app { byId(appId: $appId) { @@ -130,6 +151,8 @@ export const EnvironmentVariablesQuery = { id linkedEnvironments(appId: $appId) ...EnvironmentVariableFragment + valueWithFileContent: value(includeFileContent: $includeFileContent) + @include(if: $includeFileContent) } } } @@ -137,7 +160,7 @@ export const EnvironmentVariablesQuery = { } ${print(EnvironmentVariableFragmentNode)} `, - { appId, filterNames, environment }, + { appId, filterNames, environment, includeFileContent }, { additionalTypenames: ['EnvironmentVariable'] } ) .toPromise() @@ -151,8 +174,14 @@ export const EnvironmentVariablesQuery = { appId, filterNames, environment, - }: { appId: string; filterNames?: string[]; environment?: EnvironmentVariableEnvironment } - ): Promise { + includeFileContent = false, + }: { + appId: string; + filterNames?: string[]; + environment?: EnvironmentVariableEnvironment; + includeFileContent?: boolean; + } + ): Promise { const data = await withErrorHandlingAsync( graphqlClient .query( @@ -161,6 +190,7 @@ export const EnvironmentVariablesQuery = { $appId: String! $filterNames: [String!] $environment: EnvironmentVariableEnvironment + $includeFileContent: Boolean! ) { app { byId(appId: $appId) { @@ -172,16 +202,17 @@ export const EnvironmentVariablesQuery = { environment: $environment ) { id - name - value - environments + ...EnvironmentVariableWithSecretFragment + valueWithFileContent: value(includeFileContent: $includeFileContent) + @include(if: $includeFileContent) } } } } } + ${print(EnvironmentVariableWithSecretFragmentNode)} `, - { appId, filterNames, environment }, + { appId, filterNames, environment, includeFileContent }, { additionalTypenames: ['EnvironmentVariableWithSecret'] } ) .toPromise() diff --git a/packages/eas-cli/src/graphql/types/EnvironmentVariable.ts b/packages/eas-cli/src/graphql/types/EnvironmentVariable.ts index 47927460ea..de78447914 100644 --- a/packages/eas-cli/src/graphql/types/EnvironmentVariable.ts +++ b/packages/eas-cli/src/graphql/types/EnvironmentVariable.ts @@ -10,5 +10,6 @@ export const EnvironmentVariableFragmentNode = gql` updatedAt scope visibility + type } `; diff --git a/packages/eas-cli/src/graphql/types/EnvironmentVariableWithSecret.ts b/packages/eas-cli/src/graphql/types/EnvironmentVariableWithSecret.ts new file mode 100644 index 0000000000..d13c91f223 --- /dev/null +++ b/packages/eas-cli/src/graphql/types/EnvironmentVariableWithSecret.ts @@ -0,0 +1,15 @@ +import gql from 'graphql-tag'; + +export const EnvironmentVariableWithSecretFragmentNode = gql` + fragment EnvironmentVariableWithSecretFragment on EnvironmentVariableWithSecret { + id + name + value + environments + createdAt + updatedAt + scope + visibility + type + } +`; diff --git a/packages/eas-cli/src/prompts.ts b/packages/eas-cli/src/prompts.ts index 5d60f8815b..188e114c8d 100644 --- a/packages/eas-cli/src/prompts.ts +++ b/packages/eas-cli/src/prompts.ts @@ -53,13 +53,16 @@ export async function selectAsync( choices: ExpoChoice[], config?: { options?: Options; + initial?: T; warningMessageForDisabledEntries?: string; } ): Promise { + const initial = config?.initial ? choices.findIndex(({ value }) => value === config.initial) : 0; const { value } = await promptAsync( { message, choices, + initial, name: 'value', type: 'select', warn: config?.warningMessageForDisabledEntries, diff --git a/packages/eas-cli/src/utils/prompts.ts b/packages/eas-cli/src/utils/prompts.ts index cf610321c8..9f91142e83 100644 --- a/packages/eas-cli/src/utils/prompts.ts +++ b/packages/eas-cli/src/utils/prompts.ts @@ -2,11 +2,36 @@ import chalk from 'chalk'; import capitalize from './expodash/capitalize'; import { + EnvironmentSecretType, EnvironmentVariableEnvironment, EnvironmentVariableVisibility, } from '../graphql/generated'; import { promptAsync, selectAsync } from '../prompts'; +export async function promptVariableTypeAsync( + nonInteractive: boolean, + initialType?: EnvironmentSecretType +): Promise { + if (nonInteractive) { + throw new Error('The `--type` flag must be set when running in `--non-interactive` mode.'); + } + + const options = [ + { + title: 'String', + value: EnvironmentSecretType.String, + }, + { + title: 'File', + value: EnvironmentSecretType.FileBase64, + }, + ]; + + return await selectAsync('Select the type of variable', options, { + initial: initialType, + }); +} + export async function promptVariableVisibilityAsync( nonInteractive: boolean, selectedVisibility?: EnvironmentVariableVisibility | null diff --git a/packages/eas-cli/src/utils/variableUtils.ts b/packages/eas-cli/src/utils/variableUtils.ts index 62b91519db..c8b3cfb0b2 100644 --- a/packages/eas-cli/src/utils/variableUtils.ts +++ b/packages/eas-cli/src/utils/variableUtils.ts @@ -2,17 +2,48 @@ import dateFormat from 'dateformat'; import formatFields from './formatFields'; import { + EnvironmentSecretType, EnvironmentVariableEnvironment, EnvironmentVariableFragment, EnvironmentVariableScope, + EnvironmentVariableVisibility, } from '../graphql/generated'; +import { EnvironmentVariableWithFileContent } from '../graphql/queries/EnvironmentVariablesQuery'; export function formatVariableName(variable: EnvironmentVariableFragment): string { const name = variable.name; const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared'; const environments = variable.environments?.join(', ') ?? ''; const updatedAt = variable.updatedAt ? new Date(variable.updatedAt).toLocaleString() : ''; - return `${name} | ${scope} | ${environments} | Updated at: ${updatedAt}`; + const type = variable.type === EnvironmentSecretType.FileBase64 ? 'file' : 'string'; + const visibility = variable.visibility; + return `${name} | ${scope} | ${type} | ${visibility} | ${environments} | Updated at: ${updatedAt}`; +} + +export function formatVariableValue(variable: EnvironmentVariableWithFileContent): string { + // TODO: Add Learn more link + + if (variable.value) { + return variable.value; + } + + if (variable.valueWithFileContent) { + return atob(variable.valueWithFileContent); + } + + if (variable.visibility === EnvironmentVariableVisibility.Sensitive) { + return '***** (This is a sensitive env variable. To access it, run command with --include-sensitive flag. Learn more.)'; + } + + 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.)"; + } + + 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 '*****'; } export async function performForEnvironmentsAsync( @@ -23,12 +54,21 @@ export async function performForEnvironmentsAsync( return await Promise.all(selectedEnvironments.map(env => fun(env))); } -export function formatVariable(variable: EnvironmentVariableFragment): string { +export function formatVariable(variable: EnvironmentVariableWithFileContent): string { return formatFields([ { label: 'ID', value: variable.id }, { label: 'Name', value: variable.name }, - { label: 'Value', value: variable.value ?? '(secret)' }, + { label: 'Value', value: formatVariableValue(variable) }, { label: 'Scope', value: variable.scope }, + { label: 'Visibility', value: variable.visibility ?? '' }, + { + label: 'Environments', + value: variable.environments ? variable.environments.join(', ') : '-', + }, + { + 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') }, ]);