feat: add workflow for env variables in deploy.toml
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
cargo install --path .
|
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] # шаги сборки на вашей машине
|
[projects.my-node-app.local] # шаги сборки на вашей машине
|
||||||
commands = ["npm ci", "npm run build"]
|
commands = ["npm ci", "npm run build"]
|
||||||
|
|
||||||
|
[projects.my-node-app.env] # переменные для локальных команд сборки
|
||||||
|
PUBLIC_BASE_PATH = "/my-node-app"
|
||||||
|
|
||||||
[[projects.my-node-app.sync]] # что заливать на сервер
|
[[projects.my-node-app.sync]] # что заливать на сервер
|
||||||
source = "dist" # файл или папка (относительно workdir)
|
source = "dist" # файл или папка (относительно workdir)
|
||||||
target = "/var/www/pages"
|
target = "/var/www/pages"
|
||||||
@@ -64,7 +67,7 @@ target = "/var/www/pages"
|
|||||||
commands = ["pm2 restart pages"]
|
commands = ["pm2 restart pages"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Приоритет параметров SSH: явное значение из deploy.toml → `~/.ssh/config` → дефолт.
|
Приоритет параметров SSH: deploy.toml → `~/.ssh/config` → дефолт.
|
||||||
|
|
||||||
### Windows-нюанс TOML
|
### Windows-нюанс TOML
|
||||||
|
|
||||||
@@ -94,9 +97,10 @@ workdir = 'C:\code\my-node-app'
|
|||||||
|
|
||||||
Если рядом с workdir проекта есть `.env`, его переменные (KEY=VALUE, `#`
|
Если рядом с workdir проекта есть `.env`, его переменные (KEY=VALUE, `#`
|
||||||
комментарии, кавычки, префикс `export`) доступны всем локальным командам.
|
комментарии, кавычки, префикс `export`) доступны всем локальным командам.
|
||||||
Переменные уже существующего окружения ОС имеют приоритет над `.env`.
|
|
||||||
`.env` не копируется на сервер, если не указан явно в `sync`.
|
`.env` не копируется на сервер, если не указан явно в `sync`.
|
||||||
|
|
||||||
|
Приоритет источников: `[env]` из deploy.toml → `.env` → окружение ОС.
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# .env
|
# .env
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub struct Project {
|
|||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub key_path: Option<PathBuf>,
|
pub key_path: Option<PathBuf>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub env: BTreeMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
pub local: Option<Commands>,
|
pub local: Option<Commands>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sync: Vec<SyncRule>,
|
pub sync: Vec<SyncRule>,
|
||||||
@@ -72,6 +74,14 @@ impl Config {
|
|||||||
rule.source = expand_tilde(&rule.source)
|
rule.source = expand_tilde(&rule.source)
|
||||||
.with_context(|| format!("project '{name}': invalid sync 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)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ pub fn deploy(name: &str, project: &Project, dry_run: bool) -> Result<()> {
|
|||||||
|
|
||||||
for cmd in local_cmds {
|
for cmd in local_cmds {
|
||||||
let step = Instant::now();
|
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());
|
fail_line("🟢 [Local]", &format!("running: {cmd}..."), err.to_string());
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-6
@@ -1,10 +1,11 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
pub fn execute(cmd: &str, workdir: &Path) -> Result<()> {
|
pub fn execute(cmd: &str, workdir: &Path, extra_env: &BTreeMap<String, String>) -> Result<()> {
|
||||||
let mut command = shell_command(cmd);
|
let mut command = shell_command(cmd);
|
||||||
command.current_dir(workdir);
|
command.current_dir(workdir);
|
||||||
for (key, value) in load_dotenv(workdir)? {
|
for (key, value) in load_dotenv(workdir)? {
|
||||||
@@ -12,6 +13,9 @@ pub fn execute(cmd: &str, workdir: &Path) -> Result<()> {
|
|||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (key, value) in extra_env {
|
||||||
|
command.env(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
let status = command
|
let status = command
|
||||||
.status()
|
.status()
|
||||||
@@ -125,8 +129,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn successful_commands_pass_silently() {
|
fn successful_commands_pass_silently() {
|
||||||
execute(&ok_cmd(), Path::new(".")).unwrap();
|
execute(&ok_cmd(), Path::new("."), &BTreeMap::new()).unwrap();
|
||||||
execute(&ok_cmd(), Path::new(".")).unwrap();
|
execute(&ok_cmd(), Path::new("."), &BTreeMap::new()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -140,7 +144,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let err = cmds
|
let err = cmds
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cmd| execute(cmd, &workdir))
|
.map(|cmd| execute(cmd, &workdir, &BTreeMap::new()))
|
||||||
.collect::<Result<Vec<_>>>()
|
.collect::<Result<Vec<_>>>()
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.to_string().contains("exit code 7"), "{err:#}");
|
assert!(err.to_string().contains("exit code 7"), "{err:#}");
|
||||||
@@ -155,7 +159,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn commands_run_in_workdir() {
|
fn commands_run_in_workdir() {
|
||||||
let workdir = tmpdir("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());
|
assert!(workdir.join("in-workdir.txt").exists());
|
||||||
let _ = fs::remove_dir_all(workdir);
|
let _ = fs::remove_dir_all(workdir);
|
||||||
}
|
}
|
||||||
@@ -164,7 +168,7 @@ mod tests {
|
|||||||
fn missing_workdir_is_contextual_error() {
|
fn missing_workdir_is_contextual_error() {
|
||||||
let missing = tmpdir("gone");
|
let missing = tmpdir("gone");
|
||||||
fs::remove_dir(&missing).unwrap();
|
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:#}");
|
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"));
|
assert!(String::from_utf8_lossy(&out.stdout).contains("hello-env"));
|
||||||
let _ = fs::remove_dir_all(workdir);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user