-
Notifications
You must be signed in to change notification settings - Fork 468
feat: add deploy-specific environment variable support to netlify deploy
#8413
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { Option } from 'commander' | |
| import terminalLink from 'terminal-link' | ||
|
|
||
| import { normalizeContext } from '../../utils/env/index.js' | ||
| import { findDuplicateKey, mergeDeployEnvVars, parseDeployEnvVar } from '../../utils/env/deploy-env-vars.js' | ||
| import BaseCommand from '../base-command.js' | ||
| import { chalk, logAndThrowError, warn } from '../../utils/command-helpers.js' | ||
| import type { DeployOptionValues } from './option_values.js' | ||
|
|
@@ -76,6 +77,16 @@ For detailed configuration options, see the Netlify documentation.`, | |
| 'Specify a deploy context for environment variables read during the build ("production", "deploy-preview", "branch-deploy", "dev") or `branch:your-branch` where `your-branch` is the name of a branch (default: dev)', | ||
| normalizeContext, | ||
| ) | ||
| .option( | ||
| '--env <KEY=VALUE>', | ||
| 'Set an environment variable for this deploy only. Can be specified multiple times.', | ||
|
Contributor
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. AX nit: I feel like this might be confusing if an agent sees it and tries to run like WYT about something like "Set an environment variable for this deploy only. Only available to serverless functions at runtime, not at build. Can be specified multiple times" |
||
| parseDeployEnvVar('--env'), | ||
| ) | ||
| .option( | ||
| '--secret-env <KEY=VALUE>', | ||
| 'Set a secret environment variable for this deploy only. The value is masked in the Netlify UI and API. Can be specified multiple times.', | ||
| parseDeployEnvVar('--secret-env'), | ||
| ) | ||
| .option( | ||
| '--skip-functions-cache', | ||
| 'Ignore any functions created as part of a previous `build` or `deploy` commands, forcing them to be bundled again as part of the deployment', | ||
|
|
@@ -110,6 +121,8 @@ For detailed configuration options, see the Netlify documentation.`, | |
| 'netlify deploy --auth $NETLIFY_AUTH_TOKEN', | ||
| 'netlify deploy --trigger', | ||
| 'netlify deploy --context deploy-preview', | ||
| 'netlify deploy --env "NODE_ENV=production" --env "API_URL=https://api.example.com"', | ||
| 'netlify deploy --env "NODE_ENV=production" --secret-env "DATABASE_PASSWORD=$DB_PASSWORD"', | ||
| 'netlify deploy --site-name my-new-site --team my-team # Create site and deploy', | ||
| 'netlify deploy --allow-anonymous --dir ./public --no-build # Deploy without auth', | ||
| ]) | ||
|
|
@@ -136,6 +149,17 @@ For more information about Netlify deploys, see ${terminalLink(docsUrl, docsUrl, | |
| return logAndThrowError('--context flag is only available when using the --build flag') | ||
| } | ||
|
|
||
| if (options.env != null || options.secretEnv != null) { | ||
| if (options.trigger) { | ||
| return logAndThrowError('--env and --secret-env cannot be used with --trigger') | ||
| } | ||
|
|
||
| const duplicateKey = findDuplicateKey(mergeDeployEnvVars(options.env, options.secretEnv)) | ||
| if (duplicateKey != null) { | ||
| return logAndThrowError(`Environment variable "${duplicateKey}" was specified more than once.`) | ||
| } | ||
| } | ||
|
|
||
| if (options.siteName) { | ||
| if (options.site) { | ||
| return logAndThrowError( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { InvalidArgumentError } from "commander"; | ||
|
Contributor
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Run the formatter before merge. The Format workflow fails for this file. Run 🧰 Tools🪛 GitHub Actions: Format / 0_Format.txt[error] 1-1: oxfmt formatting check failed. Run 'oxfmt' without '--check' to format this file. 🪛 GitHub Actions: Format / Format[error] 1-1: oxfmt formatting check failed. Run 'oxfmt' without '--check' to format this file. 🤖 Prompt for AI AgentsSource: Pipeline failures |
||
|
|
||
| export interface DeployEnvironmentVariable { | ||
| key: string; | ||
| value: string; | ||
| is_secret: boolean; | ||
| scopes: ["functions"]; | ||
| } | ||
|
|
||
| const MAX_KEY_LENGTH = 255; | ||
| const VALID_KEY_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/; | ||
|
|
||
| const RESERVED_KEY_NAMES = new Set([ | ||
| // AWS-specific env vars | ||
| "AWS_REGION", | ||
| "AWS_EXECUTION_ENV", | ||
| "AWS_LAMBDA_FUNCTION_NAME", | ||
| "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", | ||
| "AWS_LAMBDA_FUNCTION_VERSION", | ||
| "AWS_LAMBDA_LOG_GROUP_NAME", | ||
| "AWS_LAMBDA_LOG_STREAM_NAME", | ||
| "AWS_ACCESS_KEY_ID", | ||
| "AWS_SECRET_ACCESS_KEY", | ||
| "AWS_SESSION_TOKEN", | ||
| "AWS_LAMBDA_RUNTIME_API", | ||
| "NETLIFY", | ||
| "BUILD_ID", | ||
| "CONTEXT", | ||
| "REPOSITORY_URL", | ||
| "BRANCH", | ||
| "HEAD", | ||
| "COMMIT_REF", | ||
| "CACHED_COMMIT_REF", | ||
| "PULL_REQUEST", | ||
| "REVIEW_ID", | ||
| "URL", | ||
| "DEPLOY_URL", | ||
| "DEPLOY_PRIME_URL", | ||
| "DEPLOY_ID", | ||
| "SITE_NAME", | ||
| "SITE_ID", | ||
| "NETLIFY_IMAGES_CDN_DOMAIN", | ||
| "INCOMING_HOOK_TITLE", | ||
| "INCOMING_HOOK_URL", | ||
| "INCOMING_HOOK_BODY", | ||
| ]); | ||
|
|
||
| const validateKey = (key: string): void => { | ||
| if (key.length > MAX_KEY_LENGTH) { | ||
| throw new InvalidArgumentError(`Key names should be ${MAX_KEY_LENGTH.toString()} characters or less.`); | ||
| } | ||
| if (!VALID_KEY_NAME.test(key)) { | ||
| throw new InvalidArgumentError( | ||
| "Key names must start with a letter and can only consist of alphanumeric characters and underscores", | ||
| ); | ||
| } | ||
| if (RESERVED_KEY_NAMES.has(key.toUpperCase())) { | ||
| throw new InvalidArgumentError(`${key} is a reserved key name`); | ||
| } | ||
| }; | ||
|
|
||
| export type DeployEnvVarFlag = "--env" | "--secret-env"; | ||
|
|
||
| /** | ||
| * Builds a Commander argument parser that accumulates `KEY=VALUE` arguments into a list of | ||
| * deploy-scoped environment variables. This lets the flag be repeated. | ||
| * | ||
| * Throws `InvalidArgumentError` if the argument is not in `KEY=VALUE` format, or if the key would | ||
| * be rejected by Envelope. | ||
| */ | ||
| // (TODO(ndhoule): Ideally we'd let the API call perform this validation and return an | ||
| // error, but it currently does not.) | ||
| export const parseDeployEnvVar = | ||
| (flag: DeployEnvVarFlag) => | ||
| (arg: string, previous: DeployEnvironmentVariable[] = []): DeployEnvironmentVariable[] => { | ||
| const separatorIndex = arg.indexOf("="); | ||
| if (separatorIndex === -1) { | ||
| throw new InvalidArgumentError(`Invalid ${flag} value "${arg}". Expected KEY=VALUE.`); | ||
| } | ||
|
|
||
| const key = arg.slice(0, separatorIndex); | ||
| if (key === "") { | ||
| throw new InvalidArgumentError(`Invalid ${flag} value "${arg}". Expected KEY=VALUE.`); | ||
| } | ||
| validateKey(key); | ||
|
|
||
| return [ | ||
| ...previous, | ||
| { | ||
| key, | ||
| value: arg.slice(separatorIndex + 1), | ||
| is_secret: flag === "--secret-env", | ||
| // Deploy-scoped variables only take effect in the functions scope. | ||
| scopes: ["functions"], | ||
| }, | ||
| ]; | ||
| }; | ||
|
|
||
| /** | ||
| * Combines the variables collected by `--env` and `--secret-env` into the single list the API | ||
| * expects. | ||
| */ | ||
| export const mergeDeployEnvVars = ( | ||
| env: DeployEnvironmentVariable[] = [], | ||
| secretEnv: DeployEnvironmentVariable[] = [], | ||
| ): DeployEnvironmentVariable[] => [...env, ...secretEnv]; | ||
|
|
||
| /** | ||
| * Returns the first key that appears more than once, or `undefined` if all keys are unique. | ||
| * | ||
| * The API rejects duplicate keys, and a single flag's parser cannot see the other flag's values, | ||
| * so callers must check the merged list. | ||
| */ | ||
| export const findDuplicateKey = (variables: DeployEnvironmentVariable[]): string | undefined => { | ||
| const seen = new Set<string>(); | ||
|
|
||
| for (const { key } of variables) { | ||
| if (seen.has(key)) { | ||
|
Contributor
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. Should we .toUpperCase() in case there is some case sensitive ops later ? could avoid some troubleshooting for us later |
||
| return key; | ||
| } | ||
| seen.add(key); | ||
| } | ||
|
|
||
| return undefined; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
I think a couple of your changes are using the old formatter,might need to run some NPM commands to make CI happy