Skip to content

Commit

Permalink
feat: support scopes and contexts in env:set and env:unset (#4966)
Browse files Browse the repository at this point in the history
* feat: wip for scopes and contexts in env:set

* fix: ensure correct column in env:list when value is not set

* feat: support --context flag in env:set

* chore: wip

* chore: fix some edge cases

* chore: support variadic options in env:set

* chore: use cached config instead of calling getSite on env:import

* chore: support variadic options in env:unset

* test: fix tests

* test: fix tests

* docs: write examples, generate docs

* test: more tests for env:set

* test: more tests for env:unset

* chore: implement copy edits
  • Loading branch information
jasonbarry authored Aug 17, 2022
1 parent 6fad2a8 commit 1450e1e
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 68 deletions.
21 changes: 21 additions & 0 deletions docs/commands/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,22 @@ netlify env:set

**Flags**

- `context` (*production | deploy-preview | branch-deploy | dev*) - Specify a deploy context (default: all contexts)
- `scope` (*builds | functions | post_processing | runtime*) - Specify a scope (default: all scopes)
- `debug` (*boolean*) - Print debugging information
- `httpProxy` (*string*) - Proxy server address to route requests through.
- `httpProxyCertificateFilename` (*string*) - Certificate file to use when connecting using a proxy server

**Examples**

```bash
netlify env:set VAR_NAME value # set in all contexts and scopes
netlify env:set VAR_NAME value --context production
netlify env:set VAR_NAME value --context production deploy-preview
netlify env:set VAR_NAME value --scope builds
netlify env:set VAR_NAME value --scope builds functions
```

---
## `env:unset`

Expand All @@ -170,10 +182,19 @@ netlify env:unset

**Flags**

- `context` (*production | deploy-preview | branch-deploy | dev*) - Specify a deploy context (default: all contexts)
- `debug` (*boolean*) - Print debugging information
- `httpProxy` (*string*) - Proxy server address to route requests through.
- `httpProxyCertificateFilename` (*string*) - Certificate file to use when connecting using a proxy server

**Examples**

```bash
netlify env:unset VAR_NAME # unset in all contexts
netlify env:unset VAR_NAME --context production
netlify env:unset VAR_NAME --context production deploy-preview
```

---

<!-- AUTO-GENERATED-CONTENT:END -->
22 changes: 11 additions & 11 deletions src/commands/env/env-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const { exit, log, logJson, translateFromEnvelopeToMongo, translateFromMongoToEn
* @returns {Promise<boolean>}
*/
const envImport = async (fileName, options, command) => {
const { api, site } = command.netlify
const { api, cachedConfig, site } = command.netlify
const siteId = site.id

if (!siteId) {
Expand All @@ -37,10 +37,10 @@ const envImport = async (fileName, options, command) => {
return false
}

const siteData = await api.getSite({ siteId })
const { siteInfo } = cachedConfig

const importIntoService = siteData.use_envelope ? importIntoEnvelope : importIntoMongo
const finalEnv = await importIntoService({ api, importedEnv, options, siteData })
const importIntoService = siteInfo.use_envelope ? importIntoEnvelope : importIntoMongo
const finalEnv = await importIntoService({ api, importedEnv, options, siteInfo })

// Return new environment variables of site if using json flag
if (options.json) {
Expand All @@ -49,7 +49,7 @@ const envImport = async (fileName, options, command) => {
}

// List newly imported environment variables in a table
log(`site: ${siteData.name}`)
log(`site: ${siteInfo.name}`)
const table = new AsciiTable(`Imported environment variables`)

table.setHeading('Key', 'Value')
Expand All @@ -61,9 +61,9 @@ const envImport = async (fileName, options, command) => {
* Updates the imported env in the site record
* @returns {Promise<object>}
*/
const importIntoMongo = async ({ api, importedEnv, options, siteData }) => {
const { env = {} } = siteData.build_settings
const siteId = siteData.id
const importIntoMongo = async ({ api, importedEnv, options, siteInfo }) => {
const { env = {} } = siteInfo.build_settings
const siteId = siteInfo.id

const finalEnv = options.replaceExisting ? importedEnv : { ...env, ...importedEnv }

Expand All @@ -86,10 +86,10 @@ const importIntoMongo = async ({ api, importedEnv, options, siteData }) => {
* Saves the imported env in the Envelope service
* @returns {Promise<object>}
*/
const importIntoEnvelope = async ({ api, importedEnv, options, siteData }) => {
const importIntoEnvelope = async ({ api, importedEnv, options, siteInfo }) => {
// fetch env vars
const accountId = siteData.account_slug
const siteId = siteData.id
const accountId = siteInfo.account_slug
const siteId = siteInfo.id
const dotEnvKeys = Object.keys(importedEnv)
const envelopeVariables = await api.getEnvVars({ accountId, siteId })
const envelopeKeys = envelopeVariables.map(({ key }) => key)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/env/env-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const getTable = ({ environment, hideValues, scopesColumn }) => {
// Key
key,
// Value
hideValues ? MASK : variable.value,
hideValues ? MASK : variable.value || ' ',
// Scope
scopesColumn && getHumanReadableScopes(variable.scopes),
].filter(Boolean),
Expand Down
119 changes: 96 additions & 23 deletions src/commands/env/env-set.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// @ts-check
const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
const { Option } = require('commander')

const { chalk, error, log, logJson, translateFromEnvelopeToMongo } = require('../../utils')

const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing']

/**
* The env:set command
Expand All @@ -10,42 +14,64 @@ const { log, logJson, translateFromEnvelopeToMongo } = require('../../utils')
* @returns {Promise<boolean>}
*/
const envSet = async (key, value, options, command) => {
const { api, site } = command.netlify
const { context, scope } = options

const { api, cachedConfig, site } = command.netlify
const siteId = site.id

if (!siteId) {
log('No site id found, please run inside a site folder or `netlify link`')
return false
}

const siteData = await api.getSite({ siteId })
const { siteInfo } = cachedConfig
let finalEnv

// Get current environment variables set in the UI
const setInService = siteData.use_envelope ? setInEnvelope : setInMongo
const finalEnv = await setInService({ api, siteData, key, value })
if (siteInfo.use_envelope) {
finalEnv = await setInEnvelope({ api, siteInfo, key, value, context, scope })
} else if (context || scope) {
error(
`To specify a context or scope, please run ${chalk.yellow(
'netlify open:admin',
)} to open the Netlify UI and opt in to the new environment variables experience from Site settings`,
)
return false
} else {
finalEnv = await setInMongo({ api, siteInfo, key, value })
}

if (!finalEnv) {
return false
}

// Return new environment variables of site if using json flag
if (options.json) {
logJson(finalEnv)
return false
}

log(`Set environment variable ${key}=${value} for site ${siteData.name}`)
const withScope = scope ? ` scoped to ${chalk.white(scope)}` : ''
log(
`Set environment variable ${chalk.yellow(`${key}${value ? '=' : ''}${value}`)}${withScope} in the ${chalk.magenta(
context || 'all',
)} context`,
)
}

/**
* Updates the env for a site record with a new key/value pair
* @returns {Promise<object>}
*/
const setInMongo = async ({ api, key, siteData, value }) => {
const { env = {} } = siteData.build_settings
const setInMongo = async ({ api, key, siteInfo, value }) => {
const { env = {} } = siteInfo.build_settings
const newEnv = {
...env,
[key]: value,
}
// Apply environment variable updates
await api.updateSite({
siteId: siteData.id,
siteId: siteInfo.id,
body: {
build_settings: {
env: newEnv,
Expand All @@ -59,28 +85,52 @@ const setInMongo = async ({ api, key, siteData, value }) => {
* Updates the env for a site configured with Envelope with a new key/value pair
* @returns {Promise<object>}
*/
const setInEnvelope = async ({ api, key, siteData, value }) => {
const accountId = siteData.account_slug
const siteId = siteData.id
const setInEnvelope = async ({ api, context, key, scope, siteInfo, value }) => {
const accountId = siteInfo.account_slug
const siteId = siteInfo.id
// fetch envelope env vars
const envelopeVariables = await api.getEnvVars({ accountId, siteId })
const scopes = ['builds', 'functions', 'runtime', 'post_processing']
const values = [{ context: 'all', value }]
// check if we need to create or update
const exists = envelopeVariables.some((envVar) => envVar.key === key)
const method = exists ? api.updateEnvVar : api.createEnvVars
const body = exists ? { key, scopes, values } : [{ key, scopes, values }]
const contexts = context || ['all']
const scopes = scope || AVAILABLE_SCOPES

let values = contexts.map((ctx) => ({ context: ctx, value }))

const existing = envelopeVariables.find((envVar) => envVar.key === key)

const params = { accountId, siteId, key }
try {
await method({ accountId, siteId, key, body })
} catch (error) {
throw error.json ? error.json.msg : error
if (existing) {
if (!value) {
// eslint-disable-next-line prefer-destructuring
values = existing.values
}
if (context && scope) {
error(
'Setting the context and scope at the same time on an existing env var is not allowed. Run the set command separately for each update.',
)
return false
}
if (context) {
// update individual value(s)
await Promise.all(values.map((val) => api.setEnvVarValue({ ...params, body: val })))
} else {
// otherwise update whole env var
const body = { key, scopes, values }
await api.updateEnvVar({ ...params, body })
}
} else {
// create whole env var
const body = [{ key, scopes, values }]
await api.createEnvVars({ ...params, body })
}
} catch (error_) {
throw error_.json ? error_.json.msg : error_
}

const env = translateFromEnvelopeToMongo(envelopeVariables)
const env = translateFromEnvelopeToMongo(envelopeVariables, context ? context[0] : 'dev')
return {
...env,
[key]: value,
[key]: value || env[key],
}
}

Expand All @@ -94,7 +144,30 @@ const createEnvSetCommand = (program) =>
.command('env:set')
.argument('<key>', 'Environment variable key')
.argument('[value]', 'Value to set to', '')
.addOption(
new Option('-c, --context <context...>', 'Specify a deploy context (default: all contexts)').choices([
'production',
'deploy-preview',
'branch-deploy',
'dev',
]),
)
.addOption(
new Option('-s, --scope <scope...>', 'Specify a scope (default: all scopes)').choices([
'builds',
'functions',
'post_processing',
'runtime',
]),
)
.description('Set value of environment variable')
.addExamples([
'netlify env:set VAR_NAME value # set in all contexts and scopes',
'netlify env:set VAR_NAME value --context production',
'netlify env:set VAR_NAME value --context production deploy-preview',
'netlify env:set VAR_NAME value --scope builds',
'netlify env:set VAR_NAME value --scope builds functions',
])
.action(async (key, value, options, command) => {
await envSet(key, value, options, command)
})
Expand Down
Loading

1 comment on commit 1450e1e

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 222 MB

Please sign in to comment.