Commit

Author:

Hash:

Timestamp:

+285 -5 +/-9 browse

Kevin Schoon [me@kevinschoon.com]

449e42d399db5e8dcca1878c8300aa48bb8fa995

Wed, 13 May 2026 08:24:41 +0000 (4 weeks ago)

hack up some new build commands in quipu
1diff --git a/Cargo.lock b/Cargo.lock
2index 3e41c76..610876f 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -529,6 +529,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6 checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
7
8 [[package]]
9+ name = "bytecount"
10+ version = "0.6.9"
11+ source = "registry+https://github.com/rust-lang/crates.io-index"
12+ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
13+
14+ [[package]]
15 name = "byteorder"
16 version = "1.5.0"
17 source = "registry+https://github.com/rust-lang/crates.io-index"
18 @@ -2033,6 +2039,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
20
21 [[package]]
22+ name = "papergrid"
23+ version = "0.17.0"
24+ source = "registry+https://github.com/rust-lang/crates.io-index"
25+ checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1"
26+ dependencies = [
27+ "bytecount",
28+ "fnv",
29+ "unicode-width",
30+ ]
31+
32+ [[package]]
33 name = "parking_lot"
34 version = "0.12.5"
35 source = "registry+https://github.com/rust-lang/crates.io-index"
36 @@ -2126,6 +2143,28 @@ dependencies = [
37 ]
38
39 [[package]]
40+ name = "proc-macro-error-attr2"
41+ version = "2.0.0"
42+ source = "registry+https://github.com/rust-lang/crates.io-index"
43+ checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
44+ dependencies = [
45+ "proc-macro2",
46+ "quote",
47+ ]
48+
49+ [[package]]
50+ name = "proc-macro-error2"
51+ version = "2.0.1"
52+ source = "registry+https://github.com/rust-lang/crates.io-index"
53+ checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
54+ dependencies = [
55+ "proc-macro-error-attr2",
56+ "proc-macro2",
57+ "quote",
58+ "syn",
59+ ]
60+
61+ [[package]]
62 name = "proc-macro2"
63 version = "1.0.106"
64 source = "registry+https://github.com/rust-lang/crates.io-index"
65 @@ -2197,7 +2236,7 @@ dependencies = [
66 "once_cell",
67 "socket2",
68 "tracing",
69- "windows-sys 0.52.0",
70+ "windows-sys 0.59.0",
71 ]
72
73 [[package]]
74 @@ -2207,10 +2246,13 @@ dependencies = [
75 "ayllu_api",
76 "ayllu_cmd",
77 "ayllu_config",
78+ "ayllu_database",
79 "ayllu_git",
80 "reqwest",
81 "serde",
82+ "tabled",
83 "thiserror 2.0.18",
84+ "timeutil",
85 "tokio",
86 "tracing",
87 "tracing-subscriber",
88 @@ -2908,6 +2950,30 @@ dependencies = [
89 ]
90
91 [[package]]
92+ name = "tabled"
93+ version = "0.20.0"
94+ source = "registry+https://github.com/rust-lang/crates.io-index"
95+ checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d"
96+ dependencies = [
97+ "papergrid",
98+ "tabled_derive",
99+ "testing_table",
100+ ]
101+
102+ [[package]]
103+ name = "tabled_derive"
104+ version = "0.11.0"
105+ source = "registry+https://github.com/rust-lang/crates.io-index"
106+ checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846"
107+ dependencies = [
108+ "heck",
109+ "proc-macro-error2",
110+ "proc-macro2",
111+ "quote",
112+ "syn",
113+ ]
114+
115+ [[package]]
116 name = "tar"
117 version = "0.4.45"
118 source = "registry+https://github.com/rust-lang/crates.io-index"
119 @@ -2932,6 +2998,15 @@ dependencies = [
120 ]
121
122 [[package]]
123+ name = "testing_table"
124+ version = "0.3.0"
125+ source = "registry+https://github.com/rust-lang/crates.io-index"
126+ checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc"
127+ dependencies = [
128+ "unicode-width",
129+ ]
130+
131+ [[package]]
132 name = "thiserror"
133 version = "1.0.69"
134 source = "registry+https://github.com/rust-lang/crates.io-index"
135 diff --git a/crates/cmd/src/quipu.rs b/crates/cmd/src/quipu.rs
136index abb32ff..f810f15 100644
137--- a/crates/cmd/src/quipu.rs
138+++ b/crates/cmd/src/quipu.rs
139 @@ -40,6 +40,22 @@ pub enum Collections {
140 }
141
142 #[derive(clap::Subcommand, Debug, PartialEq)]
143+ pub enum Build {
144+ /// Inspect build logs
145+ Logs {
146+ manifest: Option<i32>,
147+ step: Option<i32>,
148+ },
149+ /// List builds and their current status
150+ List,
151+ /// Inspect the status of a build
152+ Inspect {
153+ /// Manifest identifier
154+ id: Option<i32>,
155+ },
156+ }
157+
158+ #[derive(clap::Subcommand, Debug, PartialEq)]
159 pub enum Subcommand {
160 /// Generate autocomplete commands for common shells
161 // Complete {
162 @@ -53,4 +69,7 @@ pub enum Subcommand {
163 /// Collection and repository management
164 #[command(subcommand)]
165 Collections(Collections),
166+ /// Build management
167+ #[command(subcommand)]
168+ Build(Build),
169 }
170 diff --git a/crates/database/src/build.rs b/crates/database/src/build.rs
171index ce26d4b..35035e0 100644
172--- a/crates/database/src/build.rs
173+++ b/crates/database/src/build.rs
174 @@ -152,6 +152,21 @@ pub mod manifests {
175 .load(self.0)
176 }
177
178+ pub fn manifest_latest_id(
179+ &mut self,
180+ collection: &str,
181+ name: &str,
182+ ) -> Result<Option<i32>, Error> {
183+ Ok(table
184+ .select(dsl::id)
185+ .filter(dsl::collection.eq(collection))
186+ .filter(dsl::name.eq(name))
187+ .order_by(dsl::id.desc())
188+ .limit(1)
189+ .get_result(self.0)
190+ .ok())
191+ }
192+
193 pub fn manifest_set_dag_content(
194 &mut self,
195 id: i32,
196 @@ -224,6 +239,16 @@ pub mod workflows {
197 .get_result(self.0)
198 }
199
200+ pub fn workflow_latest_id(&mut self, manifest_id: i32) -> Result<Option<i32>, Error> {
201+ Ok(table
202+ .select(dsl::id)
203+ .filter(dsl::manifest_id.eq(manifest_id))
204+ .order_by(dsl::id.desc())
205+ .limit(1)
206+ .get_result(self.0)
207+ .ok())
208+ }
209+
210 pub fn workflow_read(&mut self, workflow_id: i32) -> Result<Workflow, Error> {
211 table
212 .select(Workflow::as_select())
213 diff --git a/crates/timeutil/src/lib.rs b/crates/timeutil/src/lib.rs
214index daf49d1..22e96fb 100644
215--- a/crates/timeutil/src/lib.rs
216+++ b/crates/timeutil/src/lib.rs
217 @@ -58,9 +58,15 @@ pub fn friendly(seconds: u64) -> String {
218 message
219 }
220
221+ pub fn timestamp_from_epoch(seconds: i64) -> String {
222+ let ts = time::OffsetDateTime::from_unix_timestamp(seconds).unwrap();
223+ ts.format(&time::format_description::well_known::Rfc3339)
224+ .unwrap()
225+ }
226+
227 pub fn timestamp() -> String {
228 time::OffsetDateTime::now_utc()
229- .format(&time::format_description::well_known::Iso8601::DEFAULT)
230+ .format(&time::format_description::well_known::Rfc3339)
231 .unwrap()
232 }
233
234 diff --git a/quipu/Cargo.toml b/quipu/Cargo.toml
235index bc792f6..410c79a 100644
236--- a/quipu/Cargo.toml
237+++ b/quipu/Cargo.toml
238 @@ -9,8 +9,10 @@ name = "quipu"
239 [dependencies]
240 ayllu_api = { path = "../crates/api" }
241 ayllu_config = { path = "../crates/config" }
242- ayllu_cmd = {path = "../crates/cmd"}
243- ayllu_git = {path = "../crates/git"}
244+ ayllu_cmd = { path = "../crates/cmd" }
245+ ayllu_database = { path = "../crates/database" }
246+ ayllu_git = { path = "../crates/git" }
247+ timeutil = { path = "../crates/timeutil" }
248
249 tokio = { workspace = true }
250 reqwest = { workspace = true }
251 @@ -20,3 +22,4 @@ thiserror = { workspace = true }
252 serde = { workspace = true }
253 url = { workspace = true }
254 webfinger-rs = { workspace = true, features = ["reqwest"] }
255+ tabled = "0.20.0"
256 diff --git a/quipu/src/config.rs b/quipu/src/config.rs
257index be75132..000fa1e 100644
258--- a/quipu/src/config.rs
259+++ b/quipu/src/config.rs
260 @@ -2,7 +2,7 @@ use ayllu_git::Collection;
261 use serde::{Deserialize, Serialize};
262 use url::Url;
263
264- use ayllu_config::Configurable;
265+ use ayllu_config::{Configurable, Database};
266
267 #[derive(Clone, Debug, Deserialize, Serialize)]
268 pub struct Instance {
269 @@ -19,6 +19,8 @@ pub struct Quipu {
270 pub struct Config {
271 pub log_level: String,
272 pub quipu: Option<Quipu>,
273+ #[serde(default = "Database::default")]
274+ pub database: Database,
275 #[serde(default = "Vec::new")]
276 pub collections: Vec<Collection>,
277 }
278 diff --git a/quipu/src/error.rs b/quipu/src/error.rs
279index 27e9d98..ae47ae4 100644
280--- a/quipu/src/error.rs
281+++ b/quipu/src/error.rs
282 @@ -23,4 +23,8 @@ pub enum QuipuError {
283 Finger(#[from] webfinger_rs::Error),
284 #[error("Git Error: {0}")]
285 Git(#[from] ayllu_git::Error),
286+ #[error("Database: {0}")]
287+ Database(#[from] ayllu_database::Error),
288+ #[error("Migration: {0}")]
289+ Migration(#[from] ayllu_database::MigrationError),
290 }
291 diff --git a/quipu/src/main.rs b/quipu/src/main.rs
292index e83d9d5..9cc2f22 100644
293--- a/quipu/src/main.rs
294+++ b/quipu/src/main.rs
295 @@ -6,11 +6,13 @@ use url::Url;
296
297 use ayllu_cmd::quipu::{Command, Subcommand};
298 use ayllu_config::Reader;
299+ use ayllu_database::Wrapper as Database;
300
301 mod client;
302 mod config;
303 mod error;
304 mod output;
305+ mod ui;
306
307 fn get_instance(
308 cfg: &config::Config,
309 @@ -87,5 +89,71 @@ async fn main() -> Result<(), error::QuipuError> {
310 Ok(())
311 }
312 },
313+ Subcommand::Build(build) => {
314+ let mut db = Database::new_ro(&cfg.database.path)?;
315+ let mut conn = db.call();
316+ let current_path = std::env::current_dir()?.canonicalize()?;
317+ let (collection, name) = ayllu_git::collection_and_name(current_path.as_path());
318+ match build {
319+ ayllu_cmd::quipu::Build::List => {
320+ let manifests = conn.manifest_list(&collection, &name)?;
321+ ui::print_builds(manifests.as_slice());
322+ Ok(())
323+ }
324+ ayllu_cmd::quipu::Build::Inspect { id } => {
325+ let id = if let Some(id) = id {
326+ id
327+ } else {
328+ if let Some(manifest_id) = conn.manifest_latest_id(&collection, &name)? {
329+ manifest_id
330+ } else {
331+ tracing::info!("No recent builds");
332+ return Ok(());
333+ }
334+ };
335+ let summary = conn.manifest_summary(id)?;
336+ ui::print_build(&summary);
337+ Ok(())
338+ }
339+ ayllu_cmd::quipu::Build::Logs { manifest, step } => {
340+ let step_ids = if let Some(step_id) = step {
341+ vec![step_id]
342+ } else {
343+ let manifest_id = if let Some(id) = manifest {
344+ id
345+ } else {
346+ if let Some(manifest_id) =
347+ conn.manifest_latest_id(&collection, &name)?
348+ {
349+ manifest_id
350+ } else {
351+ tracing::info!("No recent builds");
352+ return Ok(());
353+ }
354+ };
355+ let summary = conn.manifest_summary(manifest_id)?;
356+ summary
357+ .workflows
358+ .iter()
359+ .fold(Vec::new(), |mut accm, (_, steps)| {
360+ accm.extend(steps.iter().map(|step| step.id).collect::<Vec<i32>>());
361+ accm
362+ })
363+ };
364+ step_ids.iter().try_for_each(|step_id| {
365+ let lines = conn.log_read(*step_id, None)?;
366+ lines.iter().for_each(|line| match line.stream {
367+ ayllu_database::build::logs::Stream::Stdout => {
368+ println!("{}", line.line)
369+ }
370+ ayllu_database::build::logs::Stream::Stderr => {
371+ eprintln!("{}", line.line)
372+ }
373+ });
374+ Ok(())
375+ })
376+ }
377+ }
378+ }
379 }
380 }
381 diff --git a/quipu/src/ui.rs b/quipu/src/ui.rs
382new file mode 100644
383index 0000000..a1026ae
384--- /dev/null
385+++ b/quipu/src/ui.rs
386 @@ -0,0 +1,78 @@
387+ use ayllu_database::build::{
388+ State,
389+ manifests::{Manifest, Summary},
390+ };
391+ use tabled::{Table, Tabled, settings::Style};
392+
393+ #[derive(Tabled)]
394+ struct ManifestItem {
395+ pub id: i32,
396+ pub state: State,
397+ pub created_at: String,
398+ pub runtime: String,
399+ }
400+
401+ #[derive(Tabled)]
402+ struct StepItem {
403+ pub id: i32,
404+ pub workflow: String,
405+ pub state: State,
406+ pub runtime: String,
407+ }
408+
409+ fn runtime(started_at: Option<i32>, finished_at: Option<i32>) -> String {
410+ match (started_at, finished_at) {
411+ (Some(started_at), Some(finished_at)) => {
412+ format!("{}s", finished_at - started_at)
413+ }
414+ _ => String::from("?"),
415+ }
416+ }
417+
418+ pub fn print_builds(manifests: &[Manifest]) {
419+ let items: Vec<ManifestItem> = manifests
420+ .iter()
421+ .map(|manifest| ManifestItem {
422+ id: manifest.id,
423+ state: manifest.state,
424+ created_at: timeutil::timestamp_from_epoch(manifest.created_at as i64),
425+ runtime: runtime(manifest.started_at, manifest.finished_at),
426+ })
427+ .collect();
428+ let mut table = Table::new(items);
429+ table.with(Style::modern());
430+ println!("{table}");
431+ }
432+
433+ pub fn print_build(summary: &Summary) {
434+ let manifest_item = ManifestItem {
435+ id: summary.manifest.id,
436+ state: summary.manifest.state,
437+ created_at: timeutil::timestamp_from_epoch(summary.manifest.created_at as i64),
438+ runtime: runtime(summary.manifest.started_at, summary.manifest.finished_at),
439+ };
440+ let mut table = Table::new(vec![manifest_item]);
441+ table.with(Style::modern());
442+ println!("{table:}");
443+ let steps: Vec<StepItem> =
444+ summary
445+ .workflows
446+ .iter()
447+ .fold(Vec::new(), |mut accm, (workflow, steps)| {
448+ let steps: Vec<StepItem> = steps
449+ .iter()
450+ .map(|step| StepItem {
451+ id: step.id,
452+ workflow: workflow.name.clone(),
453+ state: step.state,
454+ runtime: runtime(step.started_at, step.finished_at),
455+ })
456+ .collect();
457+ accm.extend(steps);
458+ accm
459+ });
460+
461+ let mut table = Table::new(steps);
462+ table.with(Style::modern());
463+ println!("{table}")
464+ }