Add :secrets map to global config
Foundation for the ci.fnl-driven mirror replacement: a generic place
to declare named secrets, parsed once at config load and ready to be
exposed to CI scripts via a (secret …) accessor in a follow-up.

Assisted-by: Claude Opus 4.7 via Claude Code
change zwyqwlyqswnxmvxpttympkyuxrskxkst
commit efe0914bb8732d38cd33691de3435841f945763c
author Alpha Chen <alpha@kejadlen.dev>
date
parent mquxwltp
diff --git a/src/quire.rs b/src/quire.rs
index ef87ecf..cf7748e 100644
--- a/src/quire.rs
+++ b/src/quire.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::path::{Path, PathBuf};
 
 use miette::{Context, IntoDiagnostic, Result, ensure};
@@ -15,6 +16,10 @@ pub struct GlobalConfig {
     pub github: GithubConfig,
     #[serde(default)]
     pub sentry: Option<SentryConfig>,
+    /// Named secrets exposed to `ci.fnl` jobs as `(secret :name)`.
+    /// Each value is a `SecretString` (plain literal or `{:file "..."}`).
+    #[serde(default)]
+    pub secrets: HashMap<String, SecretString>,
 }
 
 #[derive(serde::Deserialize, Debug)]
@@ -701,6 +706,51 @@ mod tests {
         assert!(config.sentry.is_none());
     }
 
+    #[test]
+    fn global_config_secrets_default_empty() {
+        let dir = tempfile::tempdir().expect("tempdir");
+        let config_path = dir.path().join("config.fnl");
+        fs_err::write(&config_path, r#"{:github {:token "ghp_test"}}"#).expect("write");
+
+        let q = Quire {
+            base_dir: dir.path().to_path_buf(),
+        };
+        let config = q.global_config().expect("global_config should load");
+        assert!(config.secrets.is_empty());
+    }
+
+    #[test]
+    fn global_config_loads_secrets_map() {
+        let dir = tempfile::tempdir().expect("tempdir");
+        let config_path = dir.path().join("config.fnl");
+        let secret_file = dir.path().join("gh_token");
+        fs_err::write(&secret_file, "ghp_from_file\n").expect("write secret");
+        fs_err::write(
+            &config_path,
+            format!(
+                r#"{{:github {{:token "ghp_test"}}
+                    :secrets {{:github_token {{:file "{}"}}
+                              :slack_webhook "https://hooks.slack.com/abc"}}}}"#,
+                secret_file.display()
+            ),
+        )
+        .expect("write");
+
+        let q = Quire {
+            base_dir: dir.path().to_path_buf(),
+        };
+        let config = q.global_config().expect("global_config should load");
+        assert_eq!(config.secrets.len(), 2);
+        assert_eq!(
+            config.secrets["github_token"].reveal().unwrap(),
+            "ghp_from_file"
+        );
+        assert_eq!(
+            config.secrets["slack_webhook"].reveal().unwrap(),
+            "https://hooks.slack.com/abc"
+        );
+    }
+
     /// Helper: run a git subcommand in `cwd` with hermetic env, panicking on failure.
     fn git_in(cwd: &Path, args: &[&str]) {
         let output = std::process::Command::new("git")