anodizer_core/config/
env_files.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4#[derive(Debug, Clone, Serialize, JsonSchema)]
25#[serde(untagged)]
26pub enum EnvFilesConfig {
27 List(Vec<String>),
29 TokenFiles(EnvFilesTokenConfig),
31}
32
33impl<'de> Deserialize<'de> for EnvFilesConfig {
34 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
35 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
36 match &value {
37 serde_yaml_ng::Value::Sequence(_) => {
38 let list: Vec<String> =
39 serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
40 Ok(EnvFilesConfig::List(list))
41 }
42 serde_yaml_ng::Value::Mapping(_) => {
43 let tokens: EnvFilesTokenConfig =
44 serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
45 Ok(EnvFilesConfig::TokenFiles(tokens))
46 }
47 _ => Err(serde::de::Error::custom(
48 "env_files must be an array of file paths or a mapping with token file paths",
49 )),
50 }
51 }
52}
53
54impl EnvFilesConfig {
55 pub fn as_list(&self) -> Option<&[String]> {
57 match self {
58 EnvFilesConfig::List(files) => Some(files),
59 EnvFilesConfig::TokenFiles(_) => None,
60 }
61 }
62
63 pub fn as_token_files(&self) -> Option<&EnvFilesTokenConfig> {
65 match self {
66 EnvFilesConfig::List(_) => None,
67 EnvFilesConfig::TokenFiles(tokens) => Some(tokens),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
80#[serde(default, deny_unknown_fields)]
81pub struct EnvFilesTokenConfig {
82 pub github_token: Option<String>,
84 pub gitlab_token: Option<String>,
86 pub gitea_token: Option<String>,
88}
89
90pub fn read_token_file(path: &str) -> Result<Option<String>, String> {
95 let expanded = if let Some(suffix) = path.strip_prefix("~/") {
97 if let Ok(home) = std::env::var("HOME") {
98 format!("{}/{}", home, suffix)
99 } else {
100 path.to_string()
101 }
102 } else {
103 path.to_string()
104 };
105
106 match std::fs::read_to_string(&expanded) {
107 Ok(content) => {
108 let token = content.lines().next().unwrap_or("").trim().to_string();
109 if token.is_empty() {
110 Ok(None)
111 } else {
112 Ok(Some(token))
113 }
114 }
115 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
116 Err(e) => Err(format!("failed to read token file '{}': {}", path, e)),
117 }
118}
119
120pub fn load_token_files(
130 config: &EnvFilesTokenConfig,
131 log: &crate::log::StageLogger,
132) -> Result<std::collections::HashMap<String, String>, String> {
133 let mut vars = std::collections::HashMap::new();
134
135 let github_candidates: Vec<&str> = match config.github_token.as_deref() {
139 Some(p) => vec![p],
140 None => vec![
141 "~/.config/anodizer/github_token",
142 "~/.config/goreleaser/github_token",
143 ],
144 };
145 let gitlab_candidates: Vec<&str> = match config.gitlab_token.as_deref() {
146 Some(p) => vec![p],
147 None => vec![
148 "~/.config/anodizer/gitlab_token",
149 "~/.config/goreleaser/gitlab_token",
150 ],
151 };
152 let gitea_candidates: Vec<&str> = match config.gitea_token.as_deref() {
153 Some(p) => vec![p],
154 None => vec![
155 "~/.config/anodizer/gitea_token",
156 "~/.config/goreleaser/gitea_token",
157 ],
158 };
159 let mappings: [(&str, &[&str]); 3] = [
160 ("GITHUB_TOKEN", &github_candidates),
161 ("GITLAB_TOKEN", &gitlab_candidates),
162 ("GITEA_TOKEN", &gitea_candidates),
163 ];
164
165 for (env_name, candidates) in &mappings {
166 if std::env::var(env_name)
168 .ok()
169 .filter(|v| !v.is_empty())
170 .is_some()
171 {
172 log.verbose(&format!("using {} from process environment", env_name));
173 continue;
174 }
175 for file_path in candidates.iter() {
176 match read_token_file(file_path) {
177 Ok(Some(token)) => {
178 log.verbose(&format!("loaded {} from {}", env_name, file_path));
179 vars.insert(env_name.to_string(), token);
180 break;
181 }
182 Ok(None) => {
183 }
185 Err(e) => {
186 return Err(e);
187 }
188 }
189 }
190 }
191
192 Ok(vars)
193}
194
195pub fn load_env_files(
201 files: &[String],
202 log: &crate::log::StageLogger,
203 strict: bool,
204) -> Result<std::collections::HashMap<String, String>, String> {
205 let mut vars = std::collections::HashMap::new();
206 for file_path in files {
207 let content = match std::fs::read_to_string(file_path) {
208 Ok(c) => c,
209 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
210 if strict {
211 return Err(format!("env file '{}' not found (strict mode)", file_path));
212 }
213 log.warn(&format!("env file '{}' not found, skipping", file_path));
214 continue;
215 }
216 Err(e) => {
217 return Err(format!("failed to read env file '{}': {}", file_path, e));
218 }
219 };
220 for line in content.lines() {
221 let trimmed = line.trim();
222 if trimmed.is_empty() || trimmed.starts_with('#') {
223 continue;
224 }
225 let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
227 if let Some((key, value)) = trimmed.split_once('=') {
228 let key = key.trim();
229 if key.is_empty() {
230 log.warn(&format!(
231 "skipping line with empty key in '{}': {}",
232 file_path,
233 line.trim()
234 ));
235 continue;
236 }
237 let value = value.trim();
238 let value = if value.len() >= 2
240 && ((value.starts_with('"') && value.ends_with('"'))
241 || (value.starts_with('\'') && value.ends_with('\'')))
242 {
243 &value[1..value.len() - 1]
244 } else {
245 value
246 };
247 vars.insert(key.to_string(), value.to_string());
248 } else {
249 log.warn(&format!(
250 "skipping line without '=' in '{}': {}",
251 file_path, trimmed
252 ));
253 }
254 }
255 }
256 Ok(vars)
257}
258
259pub use crate::env::{parse_env_entries, render_env_entries, split_env_entry};