use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum EnvFilesConfig {
List(Vec<String>),
TokenFiles(EnvFilesTokenConfig),
}
impl<'de> Deserialize<'de> for EnvFilesConfig {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_yaml_ng::Value::deserialize(deserializer)?;
match &value {
serde_yaml_ng::Value::Sequence(_) => {
let list: Vec<String> =
serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
Ok(EnvFilesConfig::List(list))
}
serde_yaml_ng::Value::Mapping(_) => {
let tokens: EnvFilesTokenConfig =
serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
Ok(EnvFilesConfig::TokenFiles(tokens))
}
_ => Err(serde::de::Error::custom(
"env_files must be an array of file paths or a mapping with token file paths",
)),
}
}
}
impl EnvFilesConfig {
pub fn as_list(&self) -> Option<&[String]> {
match self {
EnvFilesConfig::List(files) => Some(files),
EnvFilesConfig::TokenFiles(_) => None,
}
}
pub fn as_token_files(&self) -> Option<&EnvFilesTokenConfig> {
match self {
EnvFilesConfig::List(_) => None,
EnvFilesConfig::TokenFiles(tokens) => Some(tokens),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct EnvFilesTokenConfig {
pub github_token: Option<String>,
pub gitlab_token: Option<String>,
pub gitea_token: Option<String>,
}
pub fn read_token_file(path: &str) -> Result<Option<String>, String> {
let expanded = if let Some(suffix) = path.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME") {
format!("{}/{}", home, suffix)
} else {
path.to_string()
}
} else {
path.to_string()
};
match std::fs::read_to_string(&expanded) {
Ok(content) => {
let token = content.lines().next().unwrap_or("").trim().to_string();
if token.is_empty() {
Ok(None)
} else {
Ok(Some(token))
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("failed to read token file '{}': {}", path, e)),
}
}
pub fn load_token_files(
config: &EnvFilesTokenConfig,
log: &crate::log::StageLogger,
) -> Result<std::collections::HashMap<String, String>, String> {
let mut vars = std::collections::HashMap::new();
let github_candidates: Vec<&str> = match config.github_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/github_token",
"~/.config/goreleaser/github_token",
],
};
let gitlab_candidates: Vec<&str> = match config.gitlab_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/gitlab_token",
"~/.config/goreleaser/gitlab_token",
],
};
let gitea_candidates: Vec<&str> = match config.gitea_token.as_deref() {
Some(p) => vec![p],
None => vec![
"~/.config/anodizer/gitea_token",
"~/.config/goreleaser/gitea_token",
],
};
let mappings: [(&str, &[&str]); 3] = [
("GITHUB_TOKEN", &github_candidates),
("GITLAB_TOKEN", &gitlab_candidates),
("GITEA_TOKEN", &gitea_candidates),
];
for (env_name, candidates) in &mappings {
if std::env::var(env_name)
.ok()
.filter(|v| !v.is_empty())
.is_some()
{
log.verbose(&format!("using {} from process environment", env_name));
continue;
}
for file_path in candidates.iter() {
match read_token_file(file_path) {
Ok(Some(token)) => {
log.verbose(&format!("loaded {} from {}", env_name, file_path));
vars.insert(env_name.to_string(), token);
break;
}
Ok(None) => {
}
Err(e) => {
return Err(e);
}
}
}
}
Ok(vars)
}
pub fn load_env_files(
files: &[String],
log: &crate::log::StageLogger,
strict: bool,
) -> Result<std::collections::HashMap<String, String>, String> {
let mut vars = std::collections::HashMap::new();
for file_path in files {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if strict {
return Err(format!("env file '{}' not found (strict mode)", file_path));
}
log.warn(&format!("env file '{}' not found, skipping", file_path));
continue;
}
Err(e) => {
return Err(format!("failed to read env file '{}': {}", file_path, e));
}
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
if let Some((key, value)) = trimmed.split_once('=') {
let key = key.trim();
if key.is_empty() {
log.warn(&format!(
"skipping line with empty key in '{}': {}",
file_path,
line.trim()
));
continue;
}
let value = value.trim();
let value = if value.len() >= 2
&& ((value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\'')))
{
&value[1..value.len() - 1]
} else {
value
};
vars.insert(key.to_string(), value.to_string());
} else {
log.warn(&format!(
"skipping line without '=' in '{}': {}",
file_path, trimmed
));
}
}
}
Ok(vars)
}
pub use crate::env::{parse_env_entries, render_env_entries, split_env_entry};