diff --git a/techtree-manager/Cargo.lock b/techtree-manager/Cargo.lock index 05548ee..2cfb715 100644 --- a/techtree-manager/Cargo.lock +++ b/techtree-manager/Cargo.lock @@ -964,6 +964,7 @@ dependencies = [ "hashbrown", "indexmap", "serde", + "serde_derive", ] [[package]] diff --git a/techtree-manager/Cargo.toml b/techtree-manager/Cargo.toml index 4625209..bba5bc6 100644 --- a/techtree-manager/Cargo.toml +++ b/techtree-manager/Cargo.toml @@ -13,7 +13,7 @@ chrono = "0.4.41" env_logger = { version = "0.11.8", default-features = false, features = ["auto-color", "color", "humantime"] } forgejo-api = { git = "https://git.fa-fo.de/rahix/forgejo-api.git", rev = "a3f6452cfe774898a89ac66be393e5205f5e12b7" } log = "0.4.27" -petgraph = "0.8.1" +petgraph = { version = "0.8.1", features = ["serde-1"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" sha256 = "1.6.0" diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 6303643..9d6caff 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -20,7 +20,7 @@ pub struct Context { /// URL of the repository page pub repo_url: url::Url, /// URL of the repository with authentication information attached - pub repo_auth_url: url::Url, + pub repo_auth_url: Option, /// Human readable timestamp of this manager run pub timestamp: String, } @@ -30,7 +30,9 @@ async fn run() -> anyhow::Result<()> { log::warn!("Fake tree!"); event_meta::fake() } else { - event_meta::get_issue_event_meta_from_env().context("Failed reading issue event data")? + event_meta::get_issue_event_meta_from_env() + .context("Maybe you want to run with TECHTREE_FAKE=1?") + .context("Failed reading issue event data")? }; log::info!( @@ -42,13 +44,15 @@ async fn run() -> anyhow::Result<()> { let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); log::info!("Timestamp of this run is {timestamp}"); - let token = - std::env::var("GITHUB_TOKEN").context("Failed accessing GITHUB_TOKEN auth token")?; + let token = std::env::var("GITHUB_TOKEN").ok(); + if token.is_none() { + log::warn!("No GITHUB_TOKEN, only performing read-only operations!"); + } - let server_url = url::Url::parse( - &std::env::var("GITHUB_SERVER_URL") - .context("Failed reading GITHUB_SERVER_URL server url")?, - ) + let server_url = url::Url::parse(&std::env::var("GITHUB_SERVER_URL").unwrap_or_else(|_e| { + log::warn!("Using FAFO URL as a default GITHUB_SERVER_URL!"); + "https://git.fa-fo.de".to_string() + })) .context("Failed parsing GITHUB_SERVER_URL as a url")?; let repo_url = server_url @@ -57,11 +61,19 @@ async fn run() -> anyhow::Result<()> { meta.issue.repository.owner, meta.issue.repository.name )) .unwrap(); - let mut repo_auth_url = repo_url.clone(); - repo_auth_url.set_username("forgejo-actions").unwrap(); - repo_auth_url.set_password(Some(&token)).unwrap(); - let auth = forgejo_api::Auth::Token(&token); + let repo_auth_url = token.as_ref().map(|token| { + let mut repo_auth_url = repo_url.clone(); + repo_auth_url.set_username("forgejo-actions").unwrap(); + repo_auth_url.set_password(Some(&token)).unwrap(); + repo_auth_url + }); + + let auth = if let Some(token) = token.as_ref() { + forgejo_api::Auth::Token(token) + } else { + forgejo_api::Auth::None + }; let forgejo = Forgejo::new(auth, server_url).context("Could not create Forgejo API access object")?; @@ -103,9 +115,14 @@ async fn run() -> anyhow::Result<()> { let rendered_tree = render::render(&ctx, &tree) .await .context("Failed to render the techtree")?; - render::publish(&ctx, &rendered_tree) - .await - .context("Failed to publish rendered tree to git")?; + + if token.is_some() { + render::publish(&ctx, &rendered_tree) + .await + .context("Failed to publish rendered tree to git")?; + } else { + log::warn!("Skipped publishing the rendered tree."); + } // Wiki is disabled because the tree is too big for mermaid to handle if false { @@ -115,6 +132,11 @@ async fn run() -> anyhow::Result<()> { .context("Failed to update the techtree wiki page")?; } + if token.is_none() { + log::warn!("Not running issue updates without token."); + return Ok(()); + } + 'issues: for issue in tree.iter_issues() { let subtree = tree.subtree_for_issue(issue).unwrap(); let hash = subtree.stable_hash(); diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index f3d75b9..f9ebe81 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -103,7 +103,12 @@ pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::R .arg("push") .arg("--force") .arg("--quiet") - .arg(ctx.repo_auth_url.to_string()) + .arg( + ctx.repo_auth_url + .as_ref() + .expect("cannot publish without authentication token") + .to_string(), + ) .arg("HEAD:refs/heads/render") .status() .context("Failed to push rendered graph to forgejo repository")?; diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index cf693cc..f87873c 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -113,8 +113,10 @@ impl std::fmt::Display for ElementStatus { pub type ElementIndex = petgraph::graph::NodeIndex; +#[derive(serde::Serialize)] pub struct Tree { graph: petgraph::Graph, + #[serde(skip)] issue_map: BTreeMap, } @@ -158,6 +160,16 @@ impl Tree { Subtree::new_for_element(self, element) } + pub fn iter_ultimate_elements<'a>(&'a self) -> impl Iterator + 'a { + self.graph.node_indices().filter(|index| { + // If there are no incoming edges, then this is an ultimate element! + self.graph + .neighbors_directed(*index, petgraph::Direction::Incoming) + .next() + .is_none() + }) + } + pub fn to_dot(&self) -> String { let dot = petgraph::dot::Dot::with_attr_getters( &self.graph, @@ -171,9 +183,16 @@ impl Tree { &|_g, (_, element)| element.to_dot_node_attributes(None), ); + let ultimate_elements: Vec<_> = self + .iter_ultimate_elements() + .map(|idx| idx.index().to_string()) + .collect(); + let ultimate_element_list = ultimate_elements.join("; "); + format!( r#"digraph {{ ranksep=1.2 + {{ rank=same; {ultimate_element_list}; }} {:?} }} "#,