lean_ctx_patterns/
deploy.rs1use crate::ansi::strip_ansi;
17
18pub fn compress(command: &str, output: &str) -> Option<String> {
19 let c = command.trim();
20 let (tool, rest) = split_tool(c)?;
21 if !is_deploy_sub(tool, rest.trim_start()) {
22 return None;
23 }
24 Some(compress_deploy(output))
25}
26
27fn split_tool(c: &str) -> Option<(&str, &str)> {
28 for tool in [
29 "vercel", "flyctl", "fly", "wrangler", "skaffold", "supabase",
30 ] {
31 if c == tool {
32 return Some((tool, ""));
33 }
34 if let Some(rest) = c.strip_prefix(tool)
35 && rest.starts_with(' ')
36 {
37 return Some((tool, rest));
38 }
39 }
40 None
41}
42
43fn is_deploy_sub(tool: &str, rest: &str) -> bool {
44 let first = rest.split_whitespace().next().unwrap_or("");
45 match tool {
46 "vercel" => {
48 rest.is_empty() || first.starts_with('-') || matches!(first, "deploy" | "build")
49 }
50 "fly" | "flyctl" => matches!(first, "deploy" | "launch"),
51 "wrangler" => first == "deploy" || first == "publish" || rest.starts_with("pages deploy"),
52 "skaffold" => matches!(first, "run" | "build" | "deploy" | "apply"),
53 "supabase" => {
54 rest.starts_with("db push")
55 || rest.starts_with("db reset")
56 || rest.starts_with("migration up")
57 || rest.starts_with("migration repair")
58 || rest.starts_with("functions deploy")
59 }
60 _ => false,
61 }
62}
63
64fn compress_deploy(output: &str) -> String {
65 let mut kept: Vec<String> = Vec::new();
66 for raw in output.lines() {
67 let line = strip_ansi(raw);
68 let t = line.trim();
69 if t.is_empty() {
70 continue;
71 }
72 if is_signal(t) && kept.last().map(String::as_str) != Some(t) {
73 kept.push(t.to_string());
74 }
75 }
76 if kept.is_empty() {
77 return "deploy: ok".to_string();
78 }
79 let max = 30;
81 if kept.len() <= max {
82 return kept.join("\n");
83 }
84 let tail = &kept[kept.len() - max..];
85 format!(
86 "... (+{} earlier lines)\n{}",
87 kept.len() - max,
88 tail.join("\n")
89 )
90}
91
92const MARKERS: &[&str] = &[
93 "deployed",
94 "deploy complete",
95 "deployment complete",
96 "published",
97 "released",
98 "release v",
99 "current deployment",
100 "visit your",
101 "image:",
102 "uploaded",
103 "total upload",
104 "build completed",
105 "finished supabase",
106 "deployments are now",
107 "no changes",
108 "skipped",
109 "success",
110 "✓",
111 "✅",
112 "live",
113 "applied migration",
114 "applying migration",
115];
116
117fn is_signal(t: &str) -> bool {
118 if t.contains("http://") || t.contains("https://") {
119 return true;
120 }
121 let tl = t.to_ascii_lowercase();
122 if tl.contains("error")
123 || tl.contains("failed")
124 || tl.contains("panic")
125 || tl.contains("warning")
126 || t.contains('✘')
127 || t.contains('✖')
128 {
129 return true;
130 }
131 MARKERS.iter().any(|m| tl.contains(m))
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn vercel_keeps_url_drops_build_noise() {
140 let out = "Vercel CLI 33.0.0\nInspect: https://vercel.com/org/proj/abc [2s]\nInstalling dependencies...\nadded 420 packages in 12s\nBuilding...\nCompiling pages\nProduction: https://my-app.vercel.app [45s]\n";
141 let r = compress("vercel deploy --prod", out).unwrap();
142 assert!(
143 r.contains("https://my-app.vercel.app"),
144 "keeps prod url: {r}"
145 );
146 assert!(
147 r.contains("https://vercel.com/org/proj/abc"),
148 "keeps inspect url: {r}"
149 );
150 assert!(
151 !r.contains("added 420 packages"),
152 "drops install noise: {r}"
153 );
154 assert!(!r.contains("Compiling pages"), "drops build noise: {r}");
155 }
156
157 #[test]
158 fn wrangler_keeps_published_url() {
159 let out = "Total Upload: 1.2 MiB / gzip: 0.4 MiB\nUploaded my-worker (3.5 sec)\nPublished my-worker (1.2 sec)\n https://my-worker.example.workers.dev\nCurrent Deployment ID: abc-123";
160 let r = compress("wrangler deploy", out).unwrap();
161 assert!(r.contains("https://my-worker.example.workers.dev"), "{r}");
162 assert!(r.contains("Published my-worker"), "{r}");
163 }
164
165 #[test]
166 fn fly_deploy_keeps_status_and_errors() {
167 let out = "==> Building image\n--> Building image done\nWatch your deployment at https://fly.io/apps/myapp/monitoring\n 1 desired, 1 placed, 0 healthy\nError: failed to deploy: smoke checks failed";
168 let r = compress("fly deploy", out).unwrap();
169 assert!(r.contains("Error: failed to deploy"), "keeps error: {r}");
170 assert!(r.contains("https://fly.io/apps/myapp"), "keeps url: {r}");
171 }
172
173 #[test]
174 fn non_deploy_subcommands_return_none() {
175 assert!(compress("vercel ls", "deployment list").is_none());
176 assert!(compress("fly status", "status table").is_none());
177 assert!(compress("wrangler dev", "dev server").is_none());
178 assert!(compress("supabase start", "API URL: http://localhost").is_none());
179 }
180
181 #[test]
182 fn empty_is_ok() {
183 assert_eq!(compress("vercel deploy", "").unwrap(), "deploy: ok");
184 }
185}