From aa12dab7357f5dfe6a9e6f1321824e6c0f26bd68 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 23 Aug 2026 07:03:49 +0500 Subject: [PATCH] feat: add workflow for env variables in deploy.toml --- README.md | 10 +++++++--- src/config.rs | 10 ++++++++++ src/deploy.rs | 2 +- src/local.rs | 43 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fc319a2..d1fe539 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ cargo install --path . ``` -Требуется Rust 1.80+. Кроссплатформенно: Windows (MSVC), Linux, macOS. +Требуется Rust 1.88+. Кроссплатформенно: Windows (MSVC), Linux, macOS. ## Быстрый старт @@ -56,6 +56,9 @@ key_path = "~/.ssh/id_ed25519" # опционально; иначе identity_f [projects.my-node-app.local] # шаги сборки на вашей машине commands = ["npm ci", "npm run build"] +[projects.my-node-app.env] # переменные для локальных команд сборки +PUBLIC_BASE_PATH = "/my-node-app" + [[projects.my-node-app.sync]] # что заливать на сервер source = "dist" # файл или папка (относительно workdir) target = "/var/www/pages" @@ -64,7 +67,7 @@ target = "/var/www/pages" commands = ["pm2 restart pages"] ``` -Приоритет параметров SSH: явное значение из deploy.toml → `~/.ssh/config` → дефолт. +Приоритет параметров SSH: deploy.toml → `~/.ssh/config` → дефолт. ### Windows-нюанс TOML @@ -94,9 +97,10 @@ workdir = 'C:\code\my-node-app' Если рядом с workdir проекта есть `.env`, его переменные (KEY=VALUE, `#` комментарии, кавычки, префикс `export`) доступны всем локальным командам. -Переменные уже существующего окружения ОС имеют приоритет над `.env`. `.env` не копируется на сервер, если не указан явно в `sync`. +Приоритет источников: `[env]` из deploy.toml → `.env` → окружение ОС. + ```env # .env NODE_ENV=production diff --git a/src/config.rs b/src/config.rs index f5f15ca..8810f33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,6 +23,8 @@ pub struct Project { pub port: Option, pub key_path: Option, #[serde(default)] + pub env: BTreeMap, + #[serde(default)] pub local: Option, #[serde(default)] pub sync: Vec, @@ -72,6 +74,14 @@ impl Config { rule.source = expand_tilde(&rule.source) .with_context(|| format!("project '{name}': invalid sync source"))?; } + for key in project.env.keys() { + let valid = !key.is_empty() + && !key.chars().next().unwrap().is_ascii_digit() + && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + if !valid { + bail!("project '{name}': invalid env variable name '{key}'"); + } + } } Ok(self) } diff --git a/src/deploy.rs b/src/deploy.rs index 6b24e8a..8f5fd93 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -32,7 +32,7 @@ pub fn deploy(name: &str, project: &Project, dry_run: bool) -> Result<()> { for cmd in local_cmds { let step = Instant::now(); - if let Err(err) = local::execute(cmd, &project.workdir) { + if let Err(err) = local::execute(cmd, &project.workdir, &project.env) { fail_line("🟢 [Local]", &format!("running: {cmd}..."), err.to_string()); return Err(err); } diff --git a/src/local.rs b/src/local.rs index a6bb930..36b7257 100644 --- a/src/local.rs +++ b/src/local.rs @@ -1,10 +1,11 @@ +use std::collections::BTreeMap; use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; use std::fs; -pub fn execute(cmd: &str, workdir: &Path) -> Result<()> { +pub fn execute(cmd: &str, workdir: &Path, extra_env: &BTreeMap) -> Result<()> { let mut command = shell_command(cmd); command.current_dir(workdir); for (key, value) in load_dotenv(workdir)? { @@ -12,6 +13,9 @@ pub fn execute(cmd: &str, workdir: &Path) -> Result<()> { command.env(key, value); } } + for (key, value) in extra_env { + command.env(key, value); + } let status = command .status() @@ -125,8 +129,8 @@ mod tests { #[test] fn successful_commands_pass_silently() { - execute(&ok_cmd(), Path::new(".")).unwrap(); - execute(&ok_cmd(), Path::new(".")).unwrap(); + execute(&ok_cmd(), Path::new("."), &BTreeMap::new()).unwrap(); + execute(&ok_cmd(), Path::new("."), &BTreeMap::new()).unwrap(); } #[test] @@ -140,7 +144,7 @@ mod tests { ]; let err = cmds .iter() - .map(|cmd| execute(cmd, &workdir)) + .map(|cmd| execute(cmd, &workdir, &BTreeMap::new())) .collect::>>() .unwrap_err(); assert!(err.to_string().contains("exit code 7"), "{err:#}"); @@ -155,7 +159,7 @@ mod tests { #[test] fn commands_run_in_workdir() { let workdir = tmpdir("workdir"); - execute(&marker_cmd("in-workdir.txt"), &workdir).unwrap(); + execute(&marker_cmd("in-workdir.txt"), &workdir, &BTreeMap::new()).unwrap(); assert!(workdir.join("in-workdir.txt").exists()); let _ = fs::remove_dir_all(workdir); } @@ -164,7 +168,7 @@ mod tests { fn missing_workdir_is_contextual_error() { let missing = tmpdir("gone"); fs::remove_dir(&missing).unwrap(); - let err = execute(&ok_cmd(), &missing).unwrap_err(); + let err = execute(&ok_cmd(), &missing, &BTreeMap::new()).unwrap_err(); assert!(err.to_string().contains("failed to spawn"), "{err:#}"); } @@ -224,4 +228,31 @@ mod tests { assert!(String::from_utf8_lossy(&out.stdout).contains("hello-env")); let _ = fs::remove_dir_all(workdir); } + + #[test] + fn config_env_overrides_dotenv() { + let workdir = tmpdir("dotenv-precedence"); + let echo_var = if cfg!(windows) { + "echo %XBP_P_VAR%" + } else { + "echo $XBP_P_VAR" + }; + fs::write(workdir.join(".env"), "XBP_P_VAR=from-dotenv\n").unwrap(); + let mut extra = BTreeMap::new(); + extra.insert("XBP_P_VAR".to_string(), "from-config".to_string()); + let mut command = shell_command(echo_var); + command.current_dir(&workdir); + for (k, v) in load_dotenv(&workdir).unwrap() { + if std::env::var_os(&k).is_none() { + command.env(k, v); + } + } + for (k, v) in &extra { + command.env(k, v); + } + let out = command.output().unwrap(); + assert!(String::from_utf8_lossy(&out.stdout).contains("from-config")); + assert!(!String::from_utf8_lossy(&out.stdout).contains("from-dotenv")); + let _ = fs::remove_dir_all(workdir); + } }