feat(run): 选择性字段注入、dry-run 预览、默认 JSON 输出
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 2m20s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 1m4s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m13s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled

- run 新增 -s/--secret 字段过滤,只注入指定字段到子进程(最小权限)
- run 新增 --dry-run 模式,输出变量名与来源映射,不执行命令、不暴露值
- run 新增 -o 参数,dry-run 默认 JSON 输出
- 默认输出格式改为始终 json,移除 TTY 自动切换逻辑,-o text 供人类使用
- build_injected_env_map 签名从 &[SecretField] 改为 &[&SecretField]
- 更新 AGENTS.md、README.md、.vscode/tasks.json
- version: 0.9.5 → 0.9.6

Made-with: Cursor
This commit is contained in:
voson
2026-03-19 17:39:09 +08:00
parent 3a5ec92bf0
commit 955acfe9ec
9 changed files with 286 additions and 212 deletions

View File

@@ -41,8 +41,8 @@ use output::resolve_output_mode;
# Pipe-friendly (non-TTY defaults to json-compact automatically)
secrets search -n refining --kind service | jq '.[].name'
# Inject secrets into environment variables when you really need them
secrets inject -n refining --kind service --name gitea"
# Run a command with secrets injected into its child process environment
secrets run -n refining --kind service --name gitea -- printenv"
)]
struct Cli {
/// Database URL, overrides saved config (one-time override)
@@ -132,7 +132,7 @@ EXAMPLES:
#[arg(long = "tag")]
tags: Vec<String>,
/// Plaintext metadata: key=value, key:=<json>, key=@file, or nested:path@file.
/// Use key_ref=<name> to reference a shared key entry (kind=key); inject/run merge its secrets.
/// Use key_ref=<name> to reference a shared key entry (kind=key); run merges its secrets.
#[arg(long = "meta", short = 'm')]
meta: Vec<String>,
/// Secret entry: key=value, key:=<json>, key=@file, or nested:path@file
@@ -169,8 +169,7 @@ EXAMPLES:
secrets search -n refining --kind service --name gitea \\
-f metadata.url -f metadata.default_org
# Inject decrypted secrets only when needed
secrets inject -n refining --kind service --name gitea
# Run a command with decrypted secrets only when needed
secrets run -n refining --kind service --name gitea -- printenv
# Paginate large result sets
@@ -392,54 +391,33 @@ EXAMPLES:
output: Option<String>,
},
/// Print secrets as environment variables (stdout only, nothing persisted).
///
/// Outputs KEY=VALUE pairs for all matched records. Safe to pipe or eval.
#[command(after_help = "EXAMPLES:
# Print env vars for a single service
secrets inject -n refining --kind service --name gitea
# With a custom prefix
secrets inject -n refining --kind service --name gitea --prefix GITEA
# JSON output (all vars as a JSON object)
secrets inject -n refining --kind service --name gitea -o json
# Eval into current shell (use with caution)
eval $(secrets inject -n refining --kind service --name gitea)
# For entries with metadata.key_ref, referenced key's secrets are merged automatically")]
Inject {
#[arg(short, long)]
namespace: Option<String>,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
tag: Vec<String>,
/// Prefix to prepend to every variable name (uppercased automatically)
#[arg(long, default_value = "")]
prefix: String,
/// Output format: text/KEY=VALUE (default), json, json-compact
#[arg(short, long = "output")]
output: Option<String>,
},
/// Run a command with secrets injected as environment variables.
///
/// Secrets are available only to the child process; the current shell
/// environment is not modified. The process exit code is propagated.
///
/// Use -s/--secret to inject only specific fields. Use --dry-run to preview
/// which variables would be injected without executing the command.
#[command(after_help = "EXAMPLES:
# Run a script with a single service's secrets injected
secrets run -n refining --kind service --name gitea -- ./deploy.sh
# Inject only specific fields (minimal exposure)
secrets run -n refining --kind service --name aliyun \\
-s access_key_id -s access_key_secret -- aliyun ecs DescribeInstances
# Run with a tag filter (all matched records merged)
secrets run --tag production -- env | grep GITEA
# With prefix
secrets run -n refining --kind service --name gitea --prefix GITEA -- printenv
# Preview which variables would be injected (no command executed)
secrets run -n refining --kind service --name gitea --dry-run
# Preview with field filter and JSON output
secrets run -n refining --kind service --name gitea -s token --dry-run -o json
# metadata.key_ref entries get key secrets merged (e.g. server + shared PEM)")]
Run {
#[arg(short, long)]
@@ -450,11 +428,20 @@ EXAMPLES:
name: Option<String>,
#[arg(long)]
tag: Vec<String>,
/// Only inject these secret field names (repeatable). Omit to inject all fields.
#[arg(long = "secret", short = 's')]
secret_fields: Vec<String>,
/// Prefix to prepend to every variable name (uppercased automatically)
#[arg(long, default_value = "")]
prefix: String,
/// Preview variables that would be injected without executing the command
#[arg(long)]
dry_run: bool,
/// Output format for --dry-run: json (default), json-compact, text
#[arg(short, long = "output")]
output: Option<String>,
/// Command and arguments to execute with injected environment
#[arg(last = true, required = true)]
#[arg(last = true)]
command: Vec<String>,
},
@@ -770,40 +757,24 @@ async fn main() -> Result<()> {
.await?;
}
Commands::Inject {
namespace,
kind,
name,
tag,
prefix,
output,
} => {
let master_key = crypto::load_master_key()?;
let out = resolve_output_mode(output.as_deref())?;
commands::run::run_inject(
&pool,
commands::run::InjectArgs {
namespace: namespace.as_deref(),
kind: kind.as_deref(),
name: name.as_deref(),
tags: &tag,
prefix: &prefix,
output: out,
},
&master_key,
)
.await?;
}
Commands::Run {
namespace,
kind,
name,
tag,
secret_fields,
prefix,
dry_run,
output,
command,
} => {
let master_key = crypto::load_master_key()?;
let out = resolve_output_mode(output.as_deref())?;
if !dry_run && command.is_empty() {
anyhow::bail!(
"No command specified. Usage: secrets run [filter flags] -- <command> [args]"
);
}
commands::run::run_exec(
&pool,
commands::run::RunArgs {
@@ -811,7 +782,10 @@ async fn main() -> Result<()> {
kind: kind.as_deref(),
name: name.as_deref(),
tags: &tag,
secret_fields: &secret_fields,
prefix: &prefix,
dry_run,
output: out,
command: &command,
},
&master_key,