Author:
Hash:
Timestamp:
+171 -13 +/-15 browse
Kevin Schoon [me@kevinschoon.com]
e8e0015dea9405c381e2bf435cb521bf39958061
Fri, 26 Sep 2025 14:48:17 +0000 (2 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index e986d9c..2a2e3b0 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -284,6 +284,7 @@ dependencies = [ |
| 6 | "ayllu_config", |
| 7 | "ayllu_git", |
| 8 | "ayllu_identity", |
| 9 | + "build", |
| 10 | "bytes", |
| 11 | "comrak", |
| 12 | "file-mode", |
| 13 | diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml |
| 14 | index 56f20ed..0dc5007 100644 |
| 15 | --- a/ayllu/Cargo.toml |
| 16 | +++ b/ayllu/Cargo.toml |
| 17 | @@ -8,6 +8,7 @@ rust-version = "1.83.0" |
| 18 | name = "ayllu" |
| 19 | |
| 20 | [dependencies] |
| 21 | + build = { path = "../crates/build" } |
| 22 | ayllu_api = { path = "../crates/api" } |
| 23 | ayllu_cmd = { path = "../crates/cmd" } |
| 24 | ayllu_git = { path = "../crates/git" } |
| 25 | diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs |
| 26 | index 99b21b3..0674b2e 100644 |
| 27 | --- a/ayllu/src/config.rs |
| 28 | +++ b/ayllu/src/config.rs |
| 29 | @@ -250,6 +250,11 @@ pub struct Git { |
| 30 | } |
| 31 | |
| 32 | #[derive(Deserialize, Serialize, Clone, Debug)] |
| 33 | + pub struct Build { |
| 34 | + pub job_store: PathBuf, |
| 35 | + } |
| 36 | + |
| 37 | + #[derive(Deserialize, Serialize, Clone, Debug)] |
| 38 | pub struct Config { |
| 39 | #[serde(default = "Config::default_site_name")] |
| 40 | pub site_name: String, |
| 41 | @@ -286,6 +291,7 @@ pub struct Config { |
| 42 | pub lfs: Option<Lfs>, |
| 43 | #[serde(default = "Vec::new")] |
| 44 | pub identities: Vec<Identity>, |
| 45 | + pub build: Option<Build>, |
| 46 | } |
| 47 | |
| 48 | impl Configurable for Config { |
| 49 | diff --git a/ayllu/src/web2/middleware/repository.rs b/ayllu/src/web2/middleware/repository.rs |
| 50 | index 9bb229e..187f9d9 100644 |
| 51 | --- a/ayllu/src/web2/middleware/repository.rs |
| 52 | +++ b/ayllu/src/web2/middleware/repository.rs |
| 53 | @@ -39,6 +39,7 @@ pub struct Preamble { |
| 54 | pub file_path: Option<PathBuf>, |
| 55 | pub latest_commit: Option<Commit>, |
| 56 | pub latest_commit_id: Option<String>, |
| 57 | + pub job_store: Option<PathBuf>, |
| 58 | } |
| 59 | |
| 60 | impl Preamble { |
| 61 | @@ -97,6 +98,10 @@ impl Preamble { |
| 62 | file_path: file_path.map(PathBuf::from), |
| 63 | latest_commit: latest_commit.clone(), |
| 64 | latest_commit_id: latest_commit.map(|commit| commit.id), |
| 65 | + job_store: system_config |
| 66 | + .build |
| 67 | + .as_ref() |
| 68 | + .map(|cfg| cfg.job_store.to_path_buf()), |
| 69 | }) |
| 70 | } |
| 71 | |
| 72 | diff --git a/ayllu/src/web2/navigation.rs b/ayllu/src/web2/navigation.rs |
| 73 | index 1b67ddc..70c46e3 100644 |
| 74 | --- a/ayllu/src/web2/navigation.rs |
| 75 | +++ b/ayllu/src/web2/navigation.rs |
| 76 | @@ -23,8 +23,8 @@ pub fn global(current_page: &str, mail_visible: bool) -> Items { |
| 77 | nav |
| 78 | } |
| 79 | |
| 80 | - pub fn primary(current_page: &str, collection: &str, name: &str) -> Items { |
| 81 | - vec![ |
| 82 | + pub fn primary(current_page: &str, collection: &str, name: &str, builds_enabled: bool) -> Items { |
| 83 | + let mut items = vec![ |
| 84 | // ( |
| 85 | // String::from("authors"), |
| 86 | // format!("/{}/{}/authors", collection, name,), |
| 87 | @@ -50,7 +50,18 @@ pub fn primary(current_page: &str, collection: &str, name: &str) -> Items { |
| 88 | format!("/{collection}/{name}/refs"), |
| 89 | current_page == "refs", |
| 90 | ), |
| 91 | - ] |
| 92 | + ]; |
| 93 | + if builds_enabled { |
| 94 | + items.insert( |
| 95 | + 0, |
| 96 | + ( |
| 97 | + String::from("builds"), |
| 98 | + format!("/{collection}/{name}/builds"), |
| 99 | + current_page == "builds", |
| 100 | + ), |
| 101 | + ); |
| 102 | + } |
| 103 | + items |
| 104 | } |
| 105 | |
| 106 | pub fn subnav( |
| 107 | diff --git a/ayllu/src/web2/routes/blob.rs b/ayllu/src/web2/routes/blob.rs |
| 108 | index 800b3b5..0e176cf 100644 |
| 109 | --- a/ayllu/src/web2/routes/blob.rs |
| 110 | +++ b/ayllu/src/web2/routes/blob.rs |
| 111 | @@ -64,7 +64,12 @@ pub async fn serve( |
| 112 | } |
| 113 | let blob = blob.unwrap(); |
| 114 | base.title = preamble.file_name(); |
| 115 | - base.nav_elements = navigation::primary("blob", &preamble.collection_name, &preamble.repo_name); |
| 116 | + base.nav_elements = navigation::primary( |
| 117 | + "blob", |
| 118 | + &preamble.collection_name, |
| 119 | + &preamble.repo_name, |
| 120 | + preamble.job_store.is_some(), |
| 121 | + ); |
| 122 | let mime_type = mime_guess::from_path(preamble.file_path_string()).first_or_octet_stream(); |
| 123 | tracing::debug!("rendering blob with mime type: {mime_type}"); |
| 124 | let mut content: Option<String> = None; |
| 125 | diff --git a/ayllu/src/web2/routes/build.rs b/ayllu/src/web2/routes/build.rs |
| 126 | new file mode 100644 |
| 127 | index 0000000..e109c45 |
| 128 | --- /dev/null |
| 129 | +++ b/ayllu/src/web2/routes/build.rs |
| 130 | @@ -0,0 +1,83 @@ |
| 131 | + use askama::Template; |
| 132 | + use axum::extract::Path; |
| 133 | + use axum::{extract::Extension, response::Html}; |
| 134 | + use ayllu_git::Wrapper; |
| 135 | + use build::Status; |
| 136 | + |
| 137 | + use crate::config::Config; |
| 138 | + use crate::web2::error::Error; |
| 139 | + use crate::web2::middleware::repository::Preamble; |
| 140 | + use crate::web2::navigation; |
| 141 | + use crate::web2::template::Base; |
| 142 | + |
| 143 | + #[derive(askama::Template)] |
| 144 | + #[template(path = "build.html")] |
| 145 | + struct BuildTemplate { |
| 146 | + pub base: Base, |
| 147 | + pub status: Status, |
| 148 | + } |
| 149 | + |
| 150 | + pub async fn build( |
| 151 | + Extension(cfg): Extension<Config>, |
| 152 | + Extension(preamble): Extension<Preamble>, |
| 153 | + Extension(mut base): Extension<Base>, |
| 154 | + Path((_, _, build_id)): Path<(String, String, u32)>, |
| 155 | + ) -> Result<Html<String>, Error> { |
| 156 | + println!("Build ID {build_id}"); |
| 157 | + let job_store_repository = match preamble.job_store { |
| 158 | + Some(path) => Wrapper::new(path.as_path())?, |
| 159 | + None => return Err(Error::ComponentNotEnabled(String::from("Builds"))), |
| 160 | + }; |
| 161 | + let store = build::Store { |
| 162 | + collection: &preamble.collection_name, |
| 163 | + name: &preamble.repo_name, |
| 164 | + repository: &job_store_repository, |
| 165 | + email_contact: "fixme@example.org", |
| 166 | + }; |
| 167 | + let status = match store |
| 168 | + .read(Some(build_id)) |
| 169 | + .map_err(|e| Error::Message(format!("Failed to lookup build {e:?}")))? |
| 170 | + { |
| 171 | + Some(status) => status, |
| 172 | + None => return Err(Error::NotFound(format!("Cannot find build {build_id}"))), |
| 173 | + }; |
| 174 | + base.current_time = timeutil::timestamp(); |
| 175 | + base.nav_elements = navigation::primary( |
| 176 | + "builds", |
| 177 | + &preamble.collection_name, |
| 178 | + &preamble.repo_name, |
| 179 | + true, |
| 180 | + ); |
| 181 | + Ok(Html(BuildTemplate { base, status }.render()?)) |
| 182 | + } |
| 183 | + |
| 184 | + #[derive(askama::Template)] |
| 185 | + #[template(path = "builds.html")] |
| 186 | + struct BuildsTemplate { |
| 187 | + pub base: Base, |
| 188 | + } |
| 189 | + |
| 190 | + pub async fn builds( |
| 191 | + Extension(cfg): Extension<Config>, |
| 192 | + Extension(preamble): Extension<Preamble>, |
| 193 | + Extension(mut base): Extension<Base>, |
| 194 | + ) -> Result<Html<String>, Error> { |
| 195 | + let job_store_repository = match preamble.job_store { |
| 196 | + Some(path) => Wrapper::new(path.as_path())?, |
| 197 | + None => return Err(Error::ComponentNotEnabled(String::from("Builds"))), |
| 198 | + }; |
| 199 | + let store = build::Store { |
| 200 | + collection: &preamble.collection_name, |
| 201 | + name: &preamble.repo_name, |
| 202 | + repository: &job_store_repository, |
| 203 | + email_contact: "fixme@example.org", |
| 204 | + }; |
| 205 | + base.nav_elements = navigation::primary( |
| 206 | + "builds", |
| 207 | + &preamble.collection_name, |
| 208 | + &preamble.repo_name, |
| 209 | + true, |
| 210 | + ); |
| 211 | + base.current_time = timeutil::timestamp(); |
| 212 | + Ok(Html(BuildsTemplate { base }.render()?)) |
| 213 | + } |
| 214 | diff --git a/ayllu/src/web2/routes/commit.rs b/ayllu/src/web2/routes/commit.rs |
| 215 | index 17597a8..c7e3411 100644 |
| 216 | --- a/ayllu/src/web2/routes/commit.rs |
| 217 | +++ b/ayllu/src/web2/routes/commit.rs |
| 218 | @@ -34,8 +34,12 @@ pub async fn serve( |
| 219 | ) -> Result<Html<String>, Error> { |
| 220 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 221 | base.title = format!("Commit: {commit_id}"); |
| 222 | - base.nav_elements = |
| 223 | - navigation::primary("commit", &preamble.collection_name, &preamble.repo_name); |
| 224 | + base.nav_elements = navigation::primary( |
| 225 | + "commit", |
| 226 | + &preamble.collection_name, |
| 227 | + &preamble.repo_name, |
| 228 | + preamble.job_store.is_some(), |
| 229 | + ); |
| 230 | let commit = repository.commit(Some(commit_id.to_string()))?.unwrap(); |
| 231 | let note = if commit.has_note.is_some_and(|has_note| has_note) { |
| 232 | Some(repository.read_note(commit.id.as_str())?) |
| 233 | diff --git a/ayllu/src/web2/routes/log.rs b/ayllu/src/web2/routes/log.rs |
| 234 | index 4b4d750..9088ac9 100644 |
| 235 | --- a/ayllu/src/web2/routes/log.rs |
| 236 | +++ b/ayllu/src/web2/routes/log.rs |
| 237 | @@ -38,8 +38,12 @@ pub async fn serve( |
| 238 | params: Query<HashMap<String, String>>, |
| 239 | ) -> Result<Html<String>, Error> { |
| 240 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 241 | - base.nav_elements = |
| 242 | - crate::web2::navigation::primary("log", &preamble.collection_name, &preamble.repo_name); |
| 243 | + base.nav_elements = crate::web2::navigation::primary( |
| 244 | + "log", |
| 245 | + &preamble.collection_name, |
| 246 | + &preamble.repo_name, |
| 247 | + preamble.job_store.is_some(), |
| 248 | + ); |
| 249 | let subnav_elements = navigation::subnav( |
| 250 | "log", |
| 251 | &preamble.collection_name, |
| 252 | diff --git a/ayllu/src/web2/routes/mod.rs b/ayllu/src/web2/routes/mod.rs |
| 253 | index f2dca6f..7572af5 100644 |
| 254 | --- a/ayllu/src/web2/routes/mod.rs |
| 255 | +++ b/ayllu/src/web2/routes/mod.rs |
| 256 | @@ -1,6 +1,7 @@ |
| 257 | pub mod about; |
| 258 | pub mod assets; |
| 259 | pub mod blob; |
| 260 | + pub mod build; |
| 261 | pub mod commit; |
| 262 | pub mod config; |
| 263 | pub mod finger; |
| 264 | diff --git a/ayllu/src/web2/routes/refs.rs b/ayllu/src/web2/routes/refs.rs |
| 265 | index 4669293..1056280 100644 |
| 266 | --- a/ayllu/src/web2/routes/refs.rs |
| 267 | +++ b/ayllu/src/web2/routes/refs.rs |
| 268 | @@ -33,7 +33,12 @@ pub async fn refs( |
| 269 | Extension(mut base): Extension<Base>, |
| 270 | ) -> Result<Html<String>, Error> { |
| 271 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 272 | - base.nav_elements = navigation::primary("refs", &preamble.collection_name, &preamble.repo_name); |
| 273 | + base.nav_elements = navigation::primary( |
| 274 | + "refs", |
| 275 | + &preamble.collection_name, |
| 276 | + &preamble.repo_name, |
| 277 | + preamble.job_store.is_some(), |
| 278 | + ); |
| 279 | with_preamble!(base, preamble); |
| 280 | Ok(Html( |
| 281 | RefsPageTemplate { |
| 282 | @@ -64,8 +69,12 @@ pub async fn tag( |
| 283 | ) -> Result<Html<String>, Error> { |
| 284 | let repository = Wrapper::new(preamble.repo_path.as_path())?; |
| 285 | if let Some(tag) = repository.tags()?.iter().find(|tag| tag.name == tag_name) { |
| 286 | - base.nav_elements = |
| 287 | - navigation::primary("refs", &preamble.collection_name, &preamble.repo_name); |
| 288 | + base.nav_elements = navigation::primary( |
| 289 | + "refs", |
| 290 | + &preamble.collection_name, |
| 291 | + &preamble.repo_name, |
| 292 | + preamble.job_store.is_some(), |
| 293 | + ); |
| 294 | with_preamble!(base, preamble); |
| 295 | Ok(Html( |
| 296 | TagTemplate { |
| 297 | diff --git a/ayllu/src/web2/routes/repo.rs b/ayllu/src/web2/routes/repo.rs |
| 298 | index b32c723..19a21aa 100644 |
| 299 | --- a/ayllu/src/web2/routes/repo.rs |
| 300 | +++ b/ayllu/src/web2/routes/repo.rs |
| 301 | @@ -155,8 +155,12 @@ pub async fn serve( |
| 302 | preamble.collection_name, preamble.repo_name |
| 303 | ); |
| 304 | |
| 305 | - base.nav_elements = |
| 306 | - navigation::primary("project", &preamble.collection_name, &preamble.repo_name); |
| 307 | + base.nav_elements = navigation::primary( |
| 308 | + "project", |
| 309 | + &preamble.collection_name, |
| 310 | + &preamble.repo_name, |
| 311 | + preamble.job_store.is_some(), |
| 312 | + ); |
| 313 | |
| 314 | with_preamble!(base, preamble); |
| 315 | |
| 316 | diff --git a/ayllu/src/web2/server.rs b/ayllu/src/web2/server.rs |
| 317 | index 946e7ec..021ba6c 100644 |
| 318 | --- a/ayllu/src/web2/server.rs |
| 319 | +++ b/ayllu/src/web2/server.rs |
| 320 | @@ -23,6 +23,7 @@ use crate::web2::middleware::sites; |
| 321 | use crate::web2::routes::about; |
| 322 | use crate::web2::routes::assets; |
| 323 | use crate::web2::routes::blob; |
| 324 | + use crate::web2::routes::build; |
| 325 | use crate::web2::routes::commit; |
| 326 | use crate::web2::routes::config; |
| 327 | use crate::web2::routes::finger; |
| 328 | @@ -132,6 +133,8 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
| 329 | .route("/refs", routing::get(refs::refs)) |
| 330 | .route("/refs/tag/{tag_id}", routing::get(refs::tag)) |
| 331 | .route("/refs/archive/{ref_id}", routing::get(refs::archive)) |
| 332 | + .route("/builds", routing::get(build::builds)) |
| 333 | + .route("/builds/{build_id}", routing::get(build::build)) |
| 334 | // git smart http clone |
| 335 | // /(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ |
| 336 | .route("/HEAD", routing::get(git::handle)) |
| 337 | diff --git a/ayllu/templates/build.html b/ayllu/templates/build.html |
| 338 | new file mode 100644 |
| 339 | index 0000000..9728094 |
| 340 | --- /dev/null |
| 341 | +++ b/ayllu/templates/build.html |
| 342 | @@ -0,0 +1,17 @@ |
| 343 | + {% extends "base.html" %} |
| 344 | + {% block content %} |
| 345 | + <section class="build"> |
| 346 | + <pre> |
| 347 | + Success: {{ status.success }} |
| 348 | + Runtime: {{ status.runtime }} |
| 349 | + </pre> |
| 350 | + {% for workflow in status.workflows %} |
| 351 | + <section class="workflow"> |
| 352 | + {{ workflow.name }} |
| 353 | + {% for step in workflow.steps %} |
| 354 | + {{ step.runtime }} |
| 355 | + {% endfor %} |
| 356 | + </section> |
| 357 | + {% endfor %} |
| 358 | + </section> |
| 359 | + {% endblock %} |
| 360 | diff --git a/ayllu/templates/builds.html b/ayllu/templates/builds.html |
| 361 | new file mode 100644 |
| 362 | index 0000000..b080b69 |
| 363 | --- /dev/null |
| 364 | +++ b/ayllu/templates/builds.html |
| 365 | @@ -0,0 +1,4 @@ |
| 366 | + {% extends "base.html" %} |
| 367 | + {% block content %} |
| 368 | + BUILDS!!!! |
| 369 | + {% endblock %} |