vercel-push-env
Version:
The missing Vercel CLI command to push environment variables from .env files.
128 lines (123 loc) • 5.22 kB
JavaScript
import assert from 'node:assert';
import fs from 'node:fs';
import { program, Option } from 'commander';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import axios from 'axios';
program.addOption(new Option("--projectId <projectId>", "the name of your Vercel project").makeOptionMandatory(true)).addOption(new Option("--teamId <teamId>", "the unique id of your Vercel team")).addOption(new Option("--token <token>", "the access token for your Vercel account").makeOptionMandatory(true)).addOption(new Option("--targets <targets>", "the environments to target the variables, separated by comma").makeOptionMandatory(true).choices(["production", "development", "preview"]).argParser((value) => value ? value.split(",") : [])).addOption(new Option("--varType <varType>", "the type of the variables").makeOptionMandatory(true).choices(["system", "encrypted", "plain", "secret"]).default("encrypted")).addOption(new Option("--envFile <envFile>", "the path to your env file").makeOptionMandatory(true)).addOption(new Option("--apiRoot <apiRoot>", "vercel api root").makeOptionMandatory(true).default("https://api.vercel.com"));
program.parse();
const supportedEnvTargets = /* @__PURE__ */ new Set(["development", "preview", "production"]);
function getConfig() {
const config = program.opts();
for (const target of config.targets) {
assert(supportedEnvTargets.has(target), `Unknown environment '${target}' specified.`);
}
assert(fs.existsSync(config.envFile), `No file found at '${config.envFile}'.`);
return config;
}
function parseEnvFile(config) {
const content = fs.readFileSync(config.envFile, "utf8");
const envVars = dotenv.parse(content);
if (Object.keys(envVars).length === 0) {
throw new Error(`No environment variables found in '${config.envFile}'.`);
}
try {
const parsedEnvVars = dotenvExpand.expand({ ignoreProcessEnv: true, parsed: envVars });
if (!parsedEnvVars.parsed || parsedEnvVars.error) {
throw new Error("Unable to expand environment variables.");
}
const variables = Object.entries(parsedEnvVars.parsed).map(([key, value]) => ({
key,
value,
targets: config.targets
}));
return variables;
} catch (error) {
throw new Error(`Unable to parse and expand environment variables in '${config.envFile}'.`, {
cause: error instanceof Error ? error : void 0
});
}
}
function filterTarget(envs, targets) {
const targetsKey = targets.sort().join(",");
const filtered = envs.filter((env) => env.targets.sort().join(",").includes(targetsKey));
return filtered;
}
function diffEnvVars(origin, target) {
const create = [];
const update = [];
for (const targetEnv of target) {
const originEnv = origin.find((env) => env.key === targetEnv.key);
if (!originEnv) {
create.push(targetEnv);
} else if (targetEnv.value !== originEnv.value) {
update.push({ origin: originEnv, target: targetEnv });
}
}
return { create, update };
}
async function fetchVercelEnv(config) {
const { apiRoot, projectId, teamId, token } = config;
const url = `${apiRoot}/v8/projects/${projectId}/env`;
const response = await axios.get(url, {
params: { teamId, decrypt: true },
headers: { Authorization: `Bearer ${token}` }
});
return response.data.envs.map((env) => ({
id: env.id,
key: env.key,
value: env.value,
targets: env.target
}));
}
async function createVercelEnvVars(envVars, config) {
const { apiRoot, projectId, teamId, token, varType } = config;
if (envVars.length === 0) {
return;
}
let url = `${apiRoot}/v10/projects/${projectId}/env`;
if (teamId) {
url = `${url}?teamId=${teamId}`;
}
const payload = envVars.map((envVar) => ({
key: envVar.key,
target: envVar.targets,
type: varType,
value: envVar.value
}));
await axios.post(url, payload, { headers: { Authorization: `Bearer ${token}` } });
}
function updateVercelEnvVars(update, config) {
const { apiRoot, projectId, teamId, token, varType } = config;
if (update.length === 0) {
return;
}
const envVarsToUpdate = update.map(({ origin, target }) => ({
id: origin.id,
...target
}));
return envVarsToUpdate.reduce((lastPromise, envVar) => {
const { id, key, value, targets } = envVar;
return lastPromise.then(async () => {
let url = `${apiRoot}/v9/projects/${projectId}/env/${id}`;
if (teamId) {
url = `${url}?teamId=${teamId}`;
}
await axios.patch(url, { key, value, type: varType, target: targets }, { headers: { Authorization: `Bearer ${token}` } });
});
}, Promise.resolve());
}
async function start() {
const config = getConfig();
const parsedEnvs = parseEnvFile(config);
const vercelEnvs = await fetchVercelEnv(config);
const vercelEnvsToValidate = filterTarget(vercelEnvs, config.targets);
const { create, update } = diffEnvVars(vercelEnvsToValidate, parsedEnvs);
console.log("Variables to create:");
console.table(create);
console.log("Variables to update:");
console.table(update.map(({ origin, target }) => ({ id: origin.id, ...target })));
await createVercelEnvVars(create, config);
await updateVercelEnvVars(update, config);
}
start();