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 = crate::path_util::expand_tilde(path);
96
97 match std::fs::read_to_string(expanded.as_ref()) {
98 Ok(content) => {
99 let token = content.lines().next().unwrap_or("").trim().to_string();
100 if token.is_empty() {
101 Ok(None)
102 } else {
103 Ok(Some(token))
104 }
105 }
106 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
107 Err(e) => Err(format!("failed to read token file '{}': {}", path, e)),
108 }
109}
110
111pub fn load_token_files(
121 config: &EnvFilesTokenConfig,
122 log: &crate::log::StageLogger,
123) -> Result<std::collections::HashMap<String, String>, String> {
124 let mut vars = std::collections::HashMap::new();
125
126 let github_candidates: Vec<&str> = match config.github_token.as_deref() {
130 Some(p) => vec![p],
131 None => vec![
132 "~/.config/anodizer/github_token",
133 "~/.config/goreleaser/github_token",
134 ],
135 };
136 let gitlab_candidates: Vec<&str> = match config.gitlab_token.as_deref() {
137 Some(p) => vec![p],
138 None => vec![
139 "~/.config/anodizer/gitlab_token",
140 "~/.config/goreleaser/gitlab_token",
141 ],
142 };
143 let gitea_candidates: Vec<&str> = match config.gitea_token.as_deref() {
144 Some(p) => vec![p],
145 None => vec![
146 "~/.config/anodizer/gitea_token",
147 "~/.config/goreleaser/gitea_token",
148 ],
149 };
150 let mappings: [(&str, &[&str]); 3] = [
151 ("GITHUB_TOKEN", &github_candidates),
152 ("GITLAB_TOKEN", &gitlab_candidates),
153 ("GITEA_TOKEN", &gitea_candidates),
154 ];
155
156 for (env_name, candidates) in &mappings {
157 if std::env::var(env_name)
159 .ok()
160 .filter(|v| !v.is_empty())
161 .is_some()
162 {
163 log.verbose(&format!("using {} from process environment", env_name));
164 continue;
165 }
166 for file_path in candidates.iter() {
167 match read_token_file(file_path) {
168 Ok(Some(token)) => {
169 log.verbose(&format!("loaded {} from {}", env_name, file_path));
170 vars.insert(env_name.to_string(), token);
171 break;
172 }
173 Ok(None) => {
174 }
176 Err(e) => {
177 return Err(e);
178 }
179 }
180 }
181 }
182
183 Ok(vars)
184}
185
186pub fn load_env_files(
192 files: &[String],
193 log: &crate::log::StageLogger,
194 strict: bool,
195) -> Result<std::collections::HashMap<String, String>, String> {
196 let mut vars = std::collections::HashMap::new();
197 for file_path in files {
198 let content = match std::fs::read_to_string(file_path) {
199 Ok(c) => c,
200 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
201 if strict {
202 return Err(format!("env file '{}' not found (strict mode)", file_path));
203 }
204 log.warn(&format!("env file '{}' not found, skipping", file_path));
205 continue;
206 }
207 Err(e) => {
208 return Err(format!("failed to read env file '{}': {}", file_path, e));
209 }
210 };
211 for line in content.lines() {
212 let trimmed = line.trim();
213 if trimmed.is_empty() || trimmed.starts_with('#') {
214 continue;
215 }
216 let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
218 if let Some((key, value)) = trimmed.split_once('=') {
219 let key = key.trim();
220 if key.is_empty() {
221 log.warn(&format!(
222 "skipping line with empty key in '{}': {}",
223 file_path,
224 line.trim()
225 ));
226 continue;
227 }
228 let value = value.trim();
229 let value = if value.len() >= 2
231 && ((value.starts_with('"') && value.ends_with('"'))
232 || (value.starts_with('\'') && value.ends_with('\'')))
233 {
234 &value[1..value.len() - 1]
235 } else {
236 value
237 };
238 vars.insert(key.to_string(), value.to_string());
239 } else {
240 log.warn(&format!(
241 "skipping line without '=' in '{}': {}",
242 file_path, trimmed
243 ));
244 }
245 }
246 }
247 Ok(vars)
248}
249
250pub use crate::env::{parse_env_entries, render_env_entries, split_env_entry};