cloud/
almanac_dispatch.rs1use anyhow::{Context, Result};
17use std::path::Path;
18use yah_almanac::OnChangeConfig;
19
20use crate::config::CloudConfig;
21use crate::reconciler::{MesofactStaticReconciler, ReconcileCtx};
22
23pub async fn dispatch_on_change(
28 on_change: &OnChangeConfig,
29 workspace_root: &Path,
30 env: &str,
31) -> Result<()> {
32 match on_change {
33 OnChangeConfig::MesofactRebuild { service, route } => {
34 revalidate_mesofact(service, route, workspace_root, env).await
35 }
36 OnChangeConfig::Reload { service } => {
51 tracing::info!(
52 service,
53 env,
54 "almanac on_change: reload — the artifact write is the update, \
55 nothing to rebuild"
56 );
57 Ok(())
58 }
59 }
60}
61
62async fn revalidate_mesofact(
63 service_name: &str,
64 route: &str,
65 workspace_root: &Path,
66 env: &str,
67) -> Result<()> {
68 let config = CloudConfig::load(workspace_root)
69 .with_context(|| format!("loading cloud config from {}", workspace_root.display()))?;
70
71 let svc = config
72 .service(service_name)
73 .with_context(|| format!("service {service_name:?} not found in .yah/services/"))?;
74
75 let component = svc
76 .service
77 .components
78 .iter()
79 .find(|c| c.kind == "mesofact-static")
80 .with_context(|| format!("service {service_name:?} has no mesofact-static component"))?;
81
82 let mirror = svc
83 .mirrors
84 .get(env)
85 .with_context(|| format!("service {service_name:?} has no mirror for env {env:?}"))?;
86
87 let ctx = ReconcileCtx {
88 workspace_root,
89 service: &svc.service,
90 component,
91 mirror,
92 env,
93 scope: crate::reconciler::ProviderScope::singleton(),
94 };
95
96 let reconciler = MesofactStaticReconciler::new();
97 let result = reconciler.revalidate_static(ctx, route).await?;
98
99 tracing::info!(
100 service = service_name,
101 env,
102 public_url = ?result.public_url,
103 "almanac on_change: mesofact revalidate complete"
104 );
105 Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use tempfile::TempDir;
112
113 fn write_toml(path: &std::path::Path, content: &str) {
114 if let Some(parent) = path.parent() {
115 std::fs::create_dir_all(parent).unwrap();
116 }
117 std::fs::write(path, content).unwrap();
118 }
119
120 #[tokio::test]
121 async fn missing_service_returns_error() {
122 let tmp = TempDir::new().unwrap();
123 let root = tmp.path();
124 std::fs::create_dir_all(root.join(".yah")).unwrap();
129 let on_change = OnChangeConfig::MesofactRebuild {
130 service: "no-such-svc".to_string(),
131 route: "/releases".to_string(),
132 };
133 let err = dispatch_on_change(&on_change, root, "prod")
134 .await
135 .unwrap_err();
136 let msg = format!("{err:#}");
137 assert!(msg.contains("no-such-svc"), "got: {msg}");
138 }
139
140 #[tokio::test]
141 async fn missing_mirror_env_returns_error() {
142 let tmp = TempDir::new().unwrap();
143 let root = tmp.path();
144 write_toml(
146 &root.join(".yah/services/dev-yah/service.toml"),
147 r#"schema_version = 1
148name = "dev-yah"
149domain = "yah.dev"
150
151[[components]]
152id = "site"
153kind = "mesofact-static"
154path = "app/yah/web"
155role = "static"
156wave = 0
157"#,
158 );
159 let on_change = OnChangeConfig::MesofactRebuild {
160 service: "dev-yah".to_string(),
161 route: "/releases".to_string(),
162 };
163 let err = dispatch_on_change(&on_change, root, "prod")
164 .await
165 .unwrap_err();
166 let msg = format!("{err:#}");
167 assert!(msg.contains("no mirror"), "got: {msg}");
168 }
169
170 #[tokio::test]
175 async fn reload_dispatches_nothing_and_needs_no_service_config() {
176 let tmp = TempDir::new().unwrap();
177 let on_change = OnChangeConfig::Reload {
178 service: "yah-cloud-admin".to_string(),
179 };
180 dispatch_on_change(&on_change, tmp.path(), "prod")
181 .await
182 .expect("a reload must succeed against an empty workspace");
183 }
184}