Skip to content

Commit

Permalink
[cli] optional override of existing environment variables (#11348)
Browse files Browse the repository at this point in the history
## Why

Closes #11130

## Changelog

Added `--force` argument to upsert existing environment variables (#11130)

## References

- Vercel API `/v10/projects/{idOrName}/env` https://vercel.com/docs/rest-api/endpoints/projects#create-one-or-more-environment-variables
  • Loading branch information
mountainash authored and TooTallNate committed Apr 10, 2024
1 parent 139a8e1 commit 9b2cdd9
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-ducks-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vercel": patch
---

[cli] optional override of existing environment variables with --force
13 changes: 9 additions & 4 deletions packages/cli/src/commands/env/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isAPIError } from '../../util/errors-ts';
type Options = {
'--debug': boolean;
'--sensitive': boolean;
'--force': boolean;
};

export default async function add(
Expand Down Expand Up @@ -81,7 +82,7 @@ export default async function add(
);
const choices = envTargetChoices.filter(c => !existing.has(c.value));

if (choices.length === 0) {
if (choices.length === 0 && !opts['--force']) {
output.error(
`The variable ${param(
envName
Expand Down Expand Up @@ -125,6 +126,7 @@ export default async function add(
}

const type = opts['--sensitive'] ? 'sensitive' : 'encrypted';
const upsert = opts['--force'] ? 'true' : '';

const addStamp = stamp();
try {
Expand All @@ -133,6 +135,7 @@ export default async function add(
output,
client,
project.id,
upsert,
type,
envName,
envValue,
Expand All @@ -149,9 +152,11 @@ export default async function add(

output.print(
`${prependEmoji(
`Added Environment Variable ${chalk.bold(
envName
)} to Project ${chalk.bold(project.name)} ${chalk.gray(addStamp())}`,
`${
opts['--force'] ? 'Overrode' : 'Added'
} Environment Variable ${chalk.bold(envName)} to Project ${chalk.bold(
project.name
)} ${chalk.gray(addStamp())}`,
emoji('success')
)}\n`
);
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/commands/env/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export const envCommand: Command = {
deprecated: false,
multi: false,
},
{
name: 'force',
description: 'Force overwrites when a command would normally fail',
shorthand: null,
type: 'boolean',
deprecated: false,
multi: false,
},
],
examples: [],
},
Expand Down Expand Up @@ -126,6 +134,10 @@ export const envCommand: Command = {
`${packageName} env add DB_PASS production`,
],
},
{
name: 'Override an existing Environment Variable of same target (production, preview, deployment)',
value: `${packageName} env add API_TOKEN --force`,
},
{
name: 'Add a sensitive Environment Variable',
value: `${packageName} env add API_TOKEN --sensitive`,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default async function main(client: Client) {
'--environment': String,
'--git-branch': String,
'--sensitive': Boolean,
'--force': Boolean,
});
} catch (error) {
handleError(error);
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/util/env/add-env-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ export default async function addEnvRecord(
output: Output,
client: Client,
projectId: string,
upsert: string,
type: ProjectEnvType,
key: string,
value: string,
targets: ProjectEnvTarget[],
gitBranch: string
): Promise<void> {
const actionWord = upsert ? 'Overriding' : 'Adding';
output.debug(
`Adding ${type} Environment Variable ${key} to ${targets.length} targets`
`${actionWord} ${type} Environment Variable ${key} to ${targets.length} targets`
);
const body: Omit<ProjectEnvVariable, 'id'> = {
type,
Expand All @@ -26,7 +28,8 @@ export default async function addEnvRecord(
target: targets,
gitBranch: gitBranch || undefined,
};
const url = `/v8/projects/${projectId}/env`;
const args = upsert ? `?upsert=${upsert}` : '';
const url = `/v10/projects/${projectId}/env${args}`;
await client.fetch(url, {
method: 'POST',
body,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/util/env/get-env-records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default async function getEnvRecords(
query.set('source', source);
}

const url = `/v8/projects/${projectId}/env?${query}`;
const url = `/v10/projects/${projectId}/env?${query}`;

return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
}
Expand Down
28 changes: 25 additions & 3 deletions packages/cli/src/util/env/known-error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import { isErrnoException } from '@vercel/error-utils';

const knownErrorsCodes = new Set([
'PAYMENT_REQUIRED',
'BAD_REQUEST',
'SYSTEM_ENV_WITH_VALUE',
'RESERVED_ENV_VARIABLE',
'ENV_ALREADY_EXISTS',
'ENV_CONFLICT',
'ENV_SHOULD_BE_A_SECRET',
'EXISTING_KEY_AND_TARGET',
'FORBIDDEN',
'ID_NOT_FOUND',
'INVALID_KEY',
'INVALID_VALUE',
'KEY_INVALID_CHARACTERS',
'KEY_INVALID_LENGTH',
'KEY_RESERVED',
'RESERVED_ENV_VARIABLE',
'MAX_ENVS_EXCEEDED',
'MISSING_ID',
'MISSING_KEY',
'MISSING_TARGET',
'MISSING_VALUE',
'NOT_AUTHORIZED',
'NOT_DECRYPTABLE',
'SECRET_MISSING',
'SYSTEM_ENV_WITH_VALUE',
'TEAM_NOT_FOUND',
'TOO_MANY_IDS',
'TOO_MANY_KEYS',
'UNKNOWN_ERROR',
'VALUE_INVALID_LENGTH',
'VALUE_INVALID_TYPE',
]);

export function isKnownError(error: unknown) {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/util/env/remove-env-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export default async function removeEnvRecord(
): Promise<void> {
output.debug(`Removing Environment Variable ${env.key}`);

const urlProject = `/v8/projects/${projectId}/env/${env.id}`;
const url = `/v10/projects/${projectId}/env/${env.id}`;

await client.fetch<ProjectEnvVariable>(urlProject, {
await client.fetch<ProjectEnvVariable>(url, {
method: 'DELETE',
});
}
3 changes: 3 additions & 0 deletions packages/cli/test/helpers/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ module.exports = async function prepare(session, binaryPath, tmpFixturesDir) {
'project-sensitive-env-vars': {
'package.json': '{}',
},
'project-override-env-vars': {
'package.json': '{}',
},
'dev-proxy-headers-and-env': {
'package.json': JSON.stringify({}),
'server.js': `require('http').createServer((req, res) => {
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/test/integration-2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,72 @@ test('add a sensitive env var', async () => {
);
});

test('override an existing env var', async () => {
const dir = await setupE2EFixture('project-override-env-vars');
const projectName = `project-override-env-vars-${
Math.random().toString(36).split('.')[1]
}`;

// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));

const vc = execCli(binaryPath, ['link'], {
cwd: dir,
env: {
FORCE_TTY: '1',
},
});

await setupProject(vc, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});

await vc;

const link = require(path.join(dir, '.vercel/project.json'));
const options = {
env: {
VERCEL_ORG_ID: link.orgId,
VERCEL_PROJECT_ID: link.projectId,
},
};

// 1. Initial add
const addEnvCommand = execCli(
binaryPath,
['env', 'add', 'envVarName', 'production'],
options
);

await waitForPrompt(addEnvCommand, /What’s the value of [^?]+\?/);
addEnvCommand.stdin?.write('test\n');

const output = await addEnvCommand;

expect(output.exitCode, formatOutput(output)).toBe(0);
expect(output.stderr).toContain(
'Added Environment Variable envVarName to Project'
);

// 2. Override
const overrideEnvCommand = execCli(
binaryPath,
['env', 'add', 'envVarName', 'production', '--force'],
options
);

await waitForPrompt(overrideEnvCommand, /What’s the value of [^?]+\?/);
overrideEnvCommand.stdin?.write('test\n');

const outputOverride = await overrideEnvCommand;

expect(outputOverride.exitCode, formatOutput(outputOverride)).toBe(0);
expect(outputOverride.stderr).toContain(
'Overrode Environment Variable envVarName to Project'
);
});

test('whoami with `VERCEL_ORG_ID` should favor `--scope` and should error', async () => {
if (!token) {
throw new Error('Shared state "token" not set.');
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/test/mocks/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export function useProject(
});
}
);
client.scenario.get(`/v8/projects/${project.id}/env`, (req, res) => {
client.scenario.get(`/v10/projects/${project.id}/env`, (req, res) => {
const target: ProjectEnvTarget | undefined =
typeof req.query.target === 'string'
? parseEnvironment(req.query.target)
Expand All @@ -291,14 +291,14 @@ export function useProject(

res.json({ envs: targetEnvs });
});
client.scenario.post(`/v8/projects/${project.id}/env`, (req, res) => {
client.scenario.post(`/v10/projects/${project.id}/env`, (req, res) => {
const envObj = req.body;
envObj.id = envObj.key;
envs.push(envObj);
res.json({ envs });
});
client.scenario.delete(
`/v8/projects/${project.id}/env/:envId`,
`/v10/projects/${project.id}/env/:envId`,
(req, res) => {
const envId = req.params.envId;
for (const [i, env] of envs.entries()) {
Expand Down

0 comments on commit 9b2cdd9

Please sign in to comment.