feat(cli): add --yes, --secret/--no-secret and --description to variable add (#9548)

* feat(cli): add --yes, --secret/--no-secret and --description to variable add

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(cli): cover variable add create/update flag semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): warn on secret downgrade in variable add and pin preserve semantics in test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-06-12 17:22:46 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 3585716872
commit 4e9e0c024b
6 changed files with 173 additions and 20 deletions
+67 -16
View File
@@ -185,7 +185,13 @@ async function push(
}
async function add(
opts: GlobalOptions & { public?: boolean; plainSecrets?: boolean },
opts: GlobalOptions & {
public?: boolean;
plainSecrets?: boolean;
yes?: boolean;
secret?: boolean;
description?: string;
},
value: string,
remotePath: string
) {
@@ -196,6 +202,10 @@ async function add(
return;
}
// --secret/--no-secret take precedence over the legacy --public flag;
// undefined means "secret on create, preserve current setting on update"
const isSecret = opts.secret ?? (opts.public ? false : undefined);
if (
await wmill.existsVariable({
workspace: workspace.workspaceId,
@@ -203,6 +213,7 @@ async function add(
})
) {
if (
!opts.yes &&
!(await Confirm.prompt({
message: `Variable already exist, do you want to update its value?`,
default: true,
@@ -210,22 +221,46 @@ async function add(
) {
return;
}
if (isSecret === false) {
const existing = await wmill.getVariable({
workspace: workspace.workspaceId,
path: remotePath,
decryptSecret: false,
});
if (existing.is_secret) {
log.warn(
colors.yellow(
`Variable ${remotePath} is currently secret and will be downgraded to non-secret: its value will be stored in plaintext`
)
);
}
}
log.info(colors.bold.yellow("Updating variable..."));
await wmill.updateVariable({
workspace: workspace.workspaceId,
path: remotePath,
alreadyEncrypted: false, // value from CLI is always plaintext
requestBody: {
value,
...(isSecret !== undefined ? { is_secret: isSecret } : {}),
...(opts.description !== undefined
? { description: opts.description }
: {}),
},
});
} else {
log.info(colors.bold.yellow("Creating variable..."));
await wmill.createVariable({
workspace: workspace.workspaceId,
alreadyEncrypted: false, // value from CLI is always plaintext
requestBody: {
path: remotePath,
value,
is_secret: isSecret ?? true,
description: opts.description ?? "",
},
});
}
log.info(colors.bold.yellow("Pushing variable..."));
await pushVariable(
workspace.workspaceId,
remotePath + ".variable.yaml",
undefined,
{
value,
is_secret: !opts.public,
description: "",
},
true // value from CLI is always plaintext — tell API not to treat it as pre-encrypted
);
log.info(colors.bold.underline.green(`Variable ${remotePath} pushed`));
}
@@ -255,8 +290,24 @@ const command = new Command()
"Create a new variable on the remote. This will update the variable if it already exists."
)
.arguments("<value:string> <remote_path:string>")
.option(
"--yes",
"Skip confirmation prompt when updating an existing variable"
)
.option(
"--secret",
"Mark the variable as secret (default when creating a new variable)"
)
.option(
"--no-secret",
"Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)"
)
.option(
"--description <description:string>",
"Set the variable description (when updating, the existing description is preserved if not passed)"
)
.option("--plain-secrets", "Push secrets as plain text")
.option("--public", "Legacy option, use --plain-secrets instead")
.option("--public", "Legacy option, use --no-secret instead")
.action(add as any);
+5 -1
View File
@@ -6705,8 +6705,12 @@ variable related commands
- \`variable push <file_path:string> <remote_path:string>\` - Push a local variable spec. This overrides any remote versions.
- \`--plain-secrets\` - Push secrets as plain text
- \`variable add <value:string> <remote_path:string>\` - Create a new variable on the remote. This will update the variable if it already exists.
- \`--yes\` - Skip confirmation prompt when updating an existing variable
- \`--secret\` - Mark the variable as secret (default when creating a new variable)
- \`--no-secret\` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)
- \`--description <description:string>\` - Set the variable description (when updating, the existing description is preserved if not passed)
- \`--plain-secrets\` - Push secrets as plain text
- \`--public\` - Legacy option, use --plain-secrets instead
- \`--public\` - Legacy option, use --no-secret instead
### version
+86
View File
@@ -133,6 +133,92 @@ describe("variable", () => {
});
});
test("add creates secret by default and preserves fields on update", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const varPath = `f/test/add_var_${uniqueId}`;
// Create: secret by default, --description sets the description
const createResult = await backend.runCLICommand(
["variable", "add", "v1", varPath, "--description", "first desc"],
tempDir
);
expect(createResult.code).toEqual(0);
let apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/variables/get/${varPath}`
);
expect(apiResp.status).toEqual(200);
let varData = await apiResp.json();
expect(varData.is_secret).toBe(true);
expect(varData.description).toBe("first desc");
// Update with --yes only: no prompt, is_secret and description preserved
const updateResult = await backend.runCLICommand(
["variable", "add", "v2", varPath, "--yes"],
tempDir
);
expect(updateResult.code).toEqual(0);
apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/variables/get/${varPath}`
);
expect(apiResp.status).toEqual(200);
varData = await apiResp.json();
expect(varData.is_secret).toBe(true);
expect(varData.description).toBe("first desc");
expect(varData.value).toBe("v2");
// Update with --no-secret: flips to non-secret
const noSecretResult = await backend.runCLICommand(
["variable", "add", "v3", varPath, "--yes", "--no-secret"],
tempDir
);
expect(noSecretResult.code).toEqual(0);
apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/variables/get/${varPath}`
);
expect(apiResp.status).toEqual(200);
varData = await apiResp.json();
expect(varData.is_secret).toBe(false);
expect(varData.value).toBe("v3");
// Update the non-secret variable with no secret flags: is_secret must
// stay false (preserved, not re-defaulted to secret)
const preserveResult = await backend.runCLICommand(
["variable", "add", "v4", varPath, "--yes"],
tempDir
);
expect(preserveResult.code).toEqual(0);
apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/variables/get/${varPath}`
);
expect(apiResp.status).toEqual(200);
varData = await apiResp.json();
expect(varData.is_secret).toBe(false);
expect(varData.value).toBe("v4");
// Explicit --secret flips it back
const secretResult = await backend.runCLICommand(
["variable", "add", "v5", varPath, "--yes", "--secret"],
tempDir
);
expect(secretResult.code).toEqual(0);
apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/variables/get/${varPath}`
);
expect(apiResp.status).toEqual(200);
varData = await apiResp.json();
expect(varData.is_secret).toBe(true);
expect(varData.value).toBe("v5");
});
});
test("pull retrieves variables into local files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -668,8 +668,12 @@ variable related commands
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
- `--plain-secrets` - Push secrets as plain text
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
- `--yes` - Skip confirmation prompt when updating an existing variable
- `--secret` - Mark the variable as secret (default when creating a new variable)
- `--no-secret` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)
- `--description <description:string>` - Set the variable description (when updating, the existing description is preserved if not passed)
- `--plain-secrets` - Push secrets as plain text
- `--public` - Legacy option, use --plain-secrets instead
- `--public` - Legacy option, use --no-secret instead
### version
+5 -1
View File
@@ -3219,8 +3219,12 @@ variable related commands
- \`variable push <file_path:string> <remote_path:string>\` - Push a local variable spec. This overrides any remote versions.
- \`--plain-secrets\` - Push secrets as plain text
- \`variable add <value:string> <remote_path:string>\` - Create a new variable on the remote. This will update the variable if it already exists.
- \`--yes\` - Skip confirmation prompt when updating an existing variable
- \`--secret\` - Mark the variable as secret (default when creating a new variable)
- \`--no-secret\` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)
- \`--description <description:string>\` - Set the variable description (when updating, the existing description is preserved if not passed)
- \`--plain-secrets\` - Push secrets as plain text
- \`--public\` - Legacy option, use --plain-secrets instead
- \`--public\` - Legacy option, use --no-secret instead
### version
@@ -673,8 +673,12 @@ variable related commands
- `variable push <file_path:string> <remote_path:string>` - Push a local variable spec. This overrides any remote versions.
- `--plain-secrets` - Push secrets as plain text
- `variable add <value:string> <remote_path:string>` - Create a new variable on the remote. This will update the variable if it already exists.
- `--yes` - Skip confirmation prompt when updating an existing variable
- `--secret` - Mark the variable as secret (default when creating a new variable)
- `--no-secret` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)
- `--description <description:string>` - Set the variable description (when updating, the existing description is preserved if not passed)
- `--plain-secrets` - Push secrets as plain text
- `--public` - Legacy option, use --plain-secrets instead
- `--public` - Legacy option, use --no-secret instead
### version