Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/cli/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
envGetInDeploymentAction,
envListInDeployment,
envRemoveInDeployment,
envSetFromFileInDeployment,
envSetInDeployment,
} from "./lib/env.js";
import { getDeploymentSelection } from "./lib/deploymentSelection.js";
Expand All @@ -36,6 +37,23 @@ const envSet = new Command("set")
await envSetInDeployment(ctx, deployment, originalName, originalValue);
});

const envSetFromFile = new Command("set-from-file")
.usage("[options] <file>")
.arguments("<file>")
.summary("Set variables from a file")
.description(
"Set variables from a file: `npx convex env set-from-file FILE`\n" +
"The file should contain lines of the form `NAME=value`."
)
.configureHelp({ showGlobalOptions: true })
.allowExcessArguments(false)
.action(async (file, _options, cmd) => {
const options = cmd.optsWithGlobals();
const { ctx, deployment } = await selectEnvDeployment(options);
await ensureHasConvexDependency(ctx, "env set-from-file");
await envSetFromFileInDeployment(ctx, deployment, file);
});

async function selectEnvDeployment(
options: DeploymentSelectionOptions,
): Promise<{
Expand Down Expand Up @@ -127,6 +145,7 @@ export const env = new Command("env")
"By default, this sets and views variables on your dev deployment.",
)
.addCommand(envSet)
.addCommand(envSetFromFile)
.addCommand(envGet)
.addCommand(envRemove)
.addCommand(envList)
Expand Down
61 changes: 61 additions & 0 deletions src/cli/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ export async function envSetInDeployment(
}
}

export async function envSetFromFileInDeployment(
ctx: Context,
deployment: {
deploymentUrl: string;
adminKey: string;
deploymentNotice: string;
},
file: string,
) {
const changes = await parseEnvFile(ctx, file);
if (changes.length === 0) {
logMessage(`No environment variables to set in file "${file}".`);
return;
}
await callUpdateEnvironmentVariables(ctx, deployment, changes);
logFinishedStep(`Successfully set ${chalk.bold(changes.length.toString())} environment variable(s) from file "${file}"${deployment.deploymentNotice}`);
}

async function allowEqualsSyntax(
ctx: Context,
name: string,
Expand Down Expand Up @@ -66,6 +84,49 @@ async function allowEqualsSyntax(
return [name, value];
}

async function parseEnvFile(
ctx: Context,
file: string,
): Promise<EnvVarChange[]> {
let fileContents: string;
try {
fileContents = ctx.fs.readUtf8File(file);
} catch (e) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `error: failed to read file "${file}": ${e instanceof Error ? e.message : String(e)}`,
});
}
const changes: EnvVarChange[] = [];
const lines = fileContents.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === "" || line.startsWith("#")) {
continue;
}
const eqIndex = line.indexOf("=");
if (eqIndex === -1) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `error: invalid line ${i + 1} in file "${file}": missing '=' separator`,
});
}
const name = line.slice(0, eqIndex).trim();
const value = line.slice(eqIndex + 1).trim();
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `error: invalid environment variable name "${name}" on line ${i + 1} in file "${file}"`,
});
}
changes.push({ name, value });
}
return changes;
}

export async function envGetInDeploymentAction(
ctx: Context,
deployment: {
Expand Down