-
Notifications
You must be signed in to change notification settings - Fork 83
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
Implement env:init, env:load and env:unload commands #2504
base: main
Are you sure you want to change the base?
Changes from all commits
a06b598
cc7ef9e
93f42d3
d9c008f
8300369
8a840c5
cba103d
855c861
edd105f
1385327
7f6bc76
fd45f05
58f04bb
dacdfdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,231 @@ | ||||||
import spawnAsync from '@expo/spawn-async'; | ||||||
import { appendFile, pathExists, readFile, writeFile } from 'fs-extra'; | ||||||
import os from 'os'; | ||||||
import path from 'path'; | ||||||
|
||||||
import EasCommand from '../../commandUtils/EasCommand'; | ||||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; | ||||||
import Log from '../../log'; | ||||||
import { confirmAsync } from '../../prompts'; | ||||||
|
||||||
const ENVRC_TEMPLATE = | ||||||
'dotenv_if_exists .env;\ndotenv_if_exists .env.eas.local;\ndotenv_if_exists .env.local;\n'; | ||||||
|
||||||
export default class EnvironmentVariableInit extends EasCommand { | ||||||
static override description = 'setup environment variables'; | ||||||
|
||||||
static override hidden = true; | ||||||
|
||||||
static override flags = { | ||||||
...EASNonInteractiveFlag, | ||||||
}; | ||||||
|
||||||
static override contextDefinition = { | ||||||
...this.ContextOptions.ProjectDir, | ||||||
}; | ||||||
|
||||||
async runAsync(): Promise<void> { | ||||||
const { | ||||||
flags: { 'non-interactive': nonInteractive }, | ||||||
} = await this.parse(EnvironmentVariableInit); | ||||||
|
||||||
if (nonInteractive) { | ||||||
throw new Error("Non-interactive mode is not supported for 'eas env:init'"); | ||||||
} | ||||||
Comment on lines
+32
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just remove this flag from the command |
||||||
|
||||||
const { projectDir } = await this.getContextAsync(EnvironmentVariableInit, { | ||||||
nonInteractive, | ||||||
}); | ||||||
|
||||||
await this.ensureDirenvInstalledAsync(projectDir); | ||||||
await this.setupEnvrcFileAsync(projectDir); | ||||||
await this.addDirenvHookToShellConfigAsync(); | ||||||
await this.addToGitIgnoreAsync(projectDir); | ||||||
Log.log('Running direnv allow...'); | ||||||
await spawnAsync('direnv', ['allow'], { cwd: projectDir, stdio: 'inherit' }); | ||||||
} | ||||||
|
||||||
private async addDirenvHookToShellConfigAsync(): Promise<void> { | ||||||
const direnvConfig = this.getShellDirenvConfig(); | ||||||
|
||||||
if (direnvConfig && (await pathExists(direnvConfig.shellConfigPath))) { | ||||||
const { shellConfigPath, direnvHookCmd, direnvInitCmd } = direnvConfig; | ||||||
|
||||||
const confirm = await confirmAsync({ | ||||||
message: `Do you want to add the direnv hook to ${shellConfigPath}?`, | ||||||
}); | ||||||
|
||||||
if (!confirm) { | ||||||
Log.log('Skipping adding the direnv hook to the shell config'); | ||||||
Log.log('You may need to add the direnv hook to your shell config manually.'); | ||||||
Log.log('Learn more: https://direnv.net/docs/hook.html'); | ||||||
return; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
to make it exit with non-zero code in such case |
||||||
} | ||||||
|
||||||
const configContent = await readFile(shellConfigPath, 'utf8'); | ||||||
|
||||||
if (configContent.includes(direnvHookCmd)) { | ||||||
Log.log('The direnv hook is already present in the shell config'); | ||||||
return; | ||||||
Comment on lines
+68
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would throw error here as well |
||||||
} | ||||||
|
||||||
await appendFile(shellConfigPath, `\n${direnvHookCmd}\n`, 'utf8'); | ||||||
Log.log(`Added direnv hook to ${shellConfigPath}`); | ||||||
await spawnAsync(...direnvInitCmd); | ||||||
} else { | ||||||
Log.log("Unable to determine the user's shell"); | ||||||
Log.log('You may need to add the direnv hook to your shell config manually.'); | ||||||
Log.log('Learn more: https://direnv.net/docs/hook.html'); | ||||||
} | ||||||
Comment on lines
+75
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we also need to throw an error here |
||||||
} | ||||||
|
||||||
private getShellDirenvConfig(): { | ||||||
shellConfigPath: string; | ||||||
direnvHookCmd: string; | ||||||
direnvInitCmd: [string, string[]]; | ||||||
} | null { | ||||||
const shellEnv = process.env.SHELL; | ||||||
if (!shellEnv) { | ||||||
return null; | ||||||
} | ||||||
|
||||||
if (shellEnv.endsWith('bash')) { | ||||||
return { | ||||||
shellConfigPath: path.join(os.homedir(), '.bashrc'), | ||||||
direnvHookCmd: 'eval "$(direnv hook bash)"', | ||||||
direnvInitCmd: ['eval', ['"$(direnv hook bash)"']], | ||||||
}; | ||||||
} else if (shellEnv.endsWith('zsh')) { | ||||||
return { | ||||||
shellConfigPath: path.join(os.homedir(), '.zshrc'), | ||||||
direnvHookCmd: 'eval "$(direnv hook zsh)"', | ||||||
direnvInitCmd: ['eval', ['"$(direnv hook zsh)"']], | ||||||
}; | ||||||
} else if (shellEnv.endsWith('fish')) { | ||||||
return { | ||||||
shellConfigPath: path.join(os.homedir(), '.config/fish/config.fish'), | ||||||
direnvHookCmd: 'direnv hook fish | source', | ||||||
direnvInitCmd: ['eval', ['"$(direnv hook fish)"']], | ||||||
}; | ||||||
} else { | ||||||
return null; | ||||||
} | ||||||
} | ||||||
|
||||||
private async addToGitIgnoreAsync(cwd: string): Promise<void> { | ||||||
const gitIgnorePath = path.resolve(cwd, '.gitignore'); | ||||||
if (await pathExists(gitIgnorePath)) { | ||||||
const gitignoreContent = await readFile(gitIgnorePath, 'utf8'); | ||||||
|
||||||
const filesToIgnore = ['.envrc', '.env.eas.local', '.env.eas.local.original']; | ||||||
const linesToAdd = filesToIgnore.filter(file => !gitignoreContent.includes(file)); | ||||||
|
||||||
if (linesToAdd.length > 0) { | ||||||
const confirm = await confirmAsync({ | ||||||
message: `Do you want to add ${linesToAdd.join(',')} to .gitignore?`, | ||||||
}); | ||||||
if (confirm) { | ||||||
await appendFile(gitIgnorePath, linesToAdd.join('\n') + '\n', 'utf8'); | ||||||
Log.log(`${linesToAdd.join(',')} added to .gitignore`); | ||||||
} else { | ||||||
Log.log('Skipping adding .envrc and .env.local to .gitignore'); | ||||||
} | ||||||
} else { | ||||||
Log.log('.envrc and .env.local are already present in .gitignore'); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
private async setupEnvrcFileAsync(cwd: string): Promise<void> { | ||||||
const envrcPath = path.resolve(cwd, '.envrc'); | ||||||
if (await pathExists(envrcPath)) { | ||||||
Log.log('.envrc file already exists'); | ||||||
const envrcContent = await readFile(envrcPath, 'utf8'); | ||||||
if (envrcContent.includes(ENVRC_TEMPLATE)) { | ||||||
Log.log('.envrc file is already set up'); | ||||||
return; | ||||||
} | ||||||
|
||||||
const confirm = await confirmAsync({ | ||||||
message: 'Do you want to modify the existing .envrc file?', | ||||||
}); | ||||||
if (confirm) { | ||||||
Log.log('Modifying existing .envrc file...'); | ||||||
await appendFile(envrcPath, ENVRC_TEMPLATE, 'utf8'); | ||||||
Log.log('.envrc file modified'); | ||||||
} else { | ||||||
Log.log('Skipping modifying .envrc file'); | ||||||
} | ||||||
} else { | ||||||
Log.log('Creating .envrc file...'); | ||||||
await writeFile(envrcPath, ENVRC_TEMPLATE, 'utf8'); | ||||||
Log.log('.envrc file created'); | ||||||
} | ||||||
} | ||||||
|
||||||
private async ensureDirenvInstalledAsync(cwd: string): Promise<void> { | ||||||
Log.log('Checking direnv installation...'); | ||||||
try { | ||||||
await spawnAsync('direnv', ['--version']); | ||||||
Log.log('direnv is already installed'); | ||||||
} catch { | ||||||
Log.log('direnv is not installed'); | ||||||
const install = await confirmAsync({ | ||||||
message: 'Do you want EAS CLI to install direnv for you?', | ||||||
}); | ||||||
if (install) { | ||||||
await this.installDirenvAsync(cwd); | ||||||
Log.log('direnv installed'); | ||||||
} else { | ||||||
Log.error("You'll need to install direnv manually"); | ||||||
throw new Error('Aborting...'); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
private async installDirenvAsync(cwd: string): Promise<void> { | ||||||
const platform = os.platform(); | ||||||
|
||||||
let installCommand; | ||||||
let installArgs; | ||||||
|
||||||
if (platform === 'darwin') { | ||||||
installCommand = 'brew'; | ||||||
installArgs = ['install', 'direnv']; | ||||||
} else if (platform === 'linux') { | ||||||
const linuxDistribution = await spawnAsync('cat', ['/etc/os-release']); | ||||||
const stderr = linuxDistribution.stderr; | ||||||
if (stderr) { | ||||||
throw new Error(`Error reading OS release info: ${stderr}`); | ||||||
} | ||||||
|
||||||
const stdout = linuxDistribution.stdout; | ||||||
|
||||||
if (stdout.includes('Ubuntu') || stdout.includes('Debian')) { | ||||||
Log.log('Detected a Debian-based Linux distribution.'); | ||||||
installCommand = 'sudo apt-get'; | ||||||
installArgs = ['install', '-y', 'direnv']; | ||||||
} else if (stdout.includes('Fedora') || stdout.includes('CentOS')) { | ||||||
Log.log('Detected a Red Hat-based Linux distribution.'); | ||||||
installCommand = 'sudo dnf'; | ||||||
installArgs = ['install', '-y', 'direnv']; | ||||||
} else { | ||||||
throw new Error('Your Linux distribution is not supported by this script.'); | ||||||
} | ||||||
} else { | ||||||
Log.log(`Your platform (${platform}) is not supported by this script.`); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
if (!installCommand) { | ||||||
throw new Error('Failed to determine the installation command for direnv'); | ||||||
} | ||||||
|
||||||
try { | ||||||
Log.log(`Running: ${installCommand}`); | ||||||
await spawnAsync(installCommand, installArgs, { stdio: 'inherit', cwd }); | ||||||
} catch (error: any) { | ||||||
Log.error(`Failed to install direnv: ${error.message}`); | ||||||
throw error; | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||||||||||
import assert from 'assert'; | ||||||||||||||
import * as fs from 'fs-extra'; | ||||||||||||||
import { exists } from 'fs-extra'; | ||||||||||||||
import path from 'path'; | ||||||||||||||
|
||||||||||||||
import EasCommand from '../../commandUtils/EasCommand'; | ||||||||||||||
import { EASEnvironmentFlag, EASNonInteractiveFlag } from '../../commandUtils/flags'; | ||||||||||||||
import { EnvironmentVariableFragment } from '../../graphql/generated'; | ||||||||||||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery'; | ||||||||||||||
import Log from '../../log'; | ||||||||||||||
|
||||||||||||||
const EnvLocalFile = '.env.local'; | ||||||||||||||
const EnvOriginalLocalFile = `${EnvLocalFile}.original`; | ||||||||||||||
|
||||||||||||||
export default class EnvironmentVariableLoad extends EasCommand { | ||||||||||||||
static override description = 'change environment variables'; | ||||||||||||||
|
||||||||||||||
static override hidden = true; | ||||||||||||||
|
||||||||||||||
static override flags = { | ||||||||||||||
...EASEnvironmentFlag, | ||||||||||||||
...EASNonInteractiveFlag, | ||||||||||||||
}; | ||||||||||||||
static override contextDefinition = { | ||||||||||||||
...this.ContextOptions.ProjectConfig, | ||||||||||||||
...this.ContextOptions.LoggedIn, | ||||||||||||||
...this.ContextOptions.ProjectDir, | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
async runAsync(): Promise<void> { | ||||||||||||||
const { | ||||||||||||||
flags: { environment, 'non-interactive': nonInteractive }, | ||||||||||||||
} = await this.parse(EnvironmentVariableLoad); | ||||||||||||||
|
||||||||||||||
const { | ||||||||||||||
privateProjectConfig: { projectId }, | ||||||||||||||
projectDir, | ||||||||||||||
loggedIn: { graphqlClient }, | ||||||||||||||
} = await this.getContextAsync(EnvironmentVariableLoad, { | ||||||||||||||
nonInteractive, | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
assert(environment, 'Environment is required'); | ||||||||||||||
|
||||||||||||||
const envLocalFile = path.resolve(projectDir, EnvLocalFile); | ||||||||||||||
const envOriginalLocalFile = path.resolve(projectDir, EnvOriginalLocalFile); | ||||||||||||||
|
||||||||||||||
if ((await exists(envLocalFile)) && !(await exists(envOriginalLocalFile))) { | ||||||||||||||
await fs.rename(envLocalFile, envOriginalLocalFile); | ||||||||||||||
} | ||||||||||||||
Log.log('Pulling environment variables...'); | ||||||||||||||
|
||||||||||||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync( | ||||||||||||||
graphqlClient, | ||||||||||||||
{ | ||||||||||||||
appId: projectId, | ||||||||||||||
environment, | ||||||||||||||
} | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
const secretVariables = environmentVariables | ||||||||||||||
.filter(({ value }) => value === null) | ||||||||||||||
.map(({ name }) => name); | ||||||||||||||
|
||||||||||||||
const envFileContent = environmentVariables | ||||||||||||||
.filter((variable: EnvironmentVariableFragment) => variable.value !== null) | ||||||||||||||
.map((variable: EnvironmentVariableFragment) => { | ||||||||||||||
return `${variable.name}=${variable.value}`; | ||||||||||||||
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
}) | ||||||||||||||
.join('\n'); | ||||||||||||||
|
||||||||||||||
if (envFileContent.length === 0) { | ||||||||||||||
Log.warn(`No environment variables found for ${environment}.`); | ||||||||||||||
throw new Error(`Ignoring the environment.`); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
await fs.writeFile(envLocalFile, envFileContent); | ||||||||||||||
await fs.appendFile(envLocalFile, `\nEAS_CURRENT_ENVIRONMENT=${environment}\n`); | ||||||||||||||
Log.log(`Environment variables for ${environment} have been loaded.`); | ||||||||||||||
if (secretVariables.length > 0) { | ||||||||||||||
Log.addNewLineIfNone(); | ||||||||||||||
Log.warn( | ||||||||||||||
`Some variables are not available for reading. You can edit them in ${envLocalFile} manually.` | ||||||||||||||
); | ||||||||||||||
Log.warn(`Variables that are not available for reading: ${secretVariables.join(', ')}.`); | ||||||||||||||
Log.addNewLineIfNone(); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ export default class EnvironmentValuePull extends EasCommand { | |
...EASNonInteractiveFlag, | ||
path: Flags.string({ | ||
description: 'Path to the result `.env` file', | ||
default: '.env.local', | ||
default: '.env.eas.local', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH I would stick with |
||
}), | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as fs from 'fs-extra'; | ||
import { exists } from 'fs-extra'; | ||
import path from 'path'; | ||
|
||
import EasCommand from '../../commandUtils/EasCommand'; | ||
import Log from '../../log'; | ||
|
||
const EnvLocalFile = '.env.local'; | ||
const EnvOriginalLocalFile = `${EnvLocalFile}.original`; | ||
|
||
export default class EnvironmentVariableUnload extends EasCommand { | ||
static override description = 'unload environment variables'; | ||
|
||
static override hidden = true; | ||
|
||
static override contextDefinition = { | ||
...this.ContextOptions.ProjectDir, | ||
}; | ||
|
||
async runAsync(): Promise<void> { | ||
const { projectDir } = await this.getContextAsync(EnvironmentVariableUnload, { | ||
nonInteractive: true, | ||
}); | ||
|
||
const envLocalFile = path.resolve(projectDir, EnvLocalFile); | ||
const envOriginalLocalFile = path.resolve(projectDir, EnvOriginalLocalFile); | ||
|
||
if (await exists(envOriginalLocalFile)) { | ||
await fs.rename(envOriginalLocalFile, envLocalFile); | ||
} else { | ||
await fs.remove(envLocalFile); | ||
} | ||
Log.log(`Unloaded environment variables.`); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.