#![allow(dead_code)] #![allow(clippy::useless_format)] #![allow(clippy::needless_return)] #![deny(clippy::as_conversions)] use anyhow::Context as _; use forgejo_api::Forgejo; mod collect; mod event_meta; mod issue; mod render; mod tree; mod wiki; pub struct Context { /// API Accessor object pub forgejo: forgejo_api::Forgejo, /// Repository Owner pub owner: String, /// Repository Name pub repo: String, /// URL of the repository page pub repo_url: url::Url, /// URL of the repository with authentication information attached pub repo_auth_url: Option, /// Human readable timestamp of this manager run pub timestamp: String, } async fn run() -> anyhow::Result<()> { let meta = if std::env::var("TECHTREE_FAKE").ok().is_some() { log::warn!("Fake tree!"); event_meta::fake() } else { 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!( "Running due to event \"{:?}\" on issue #{} ...", meta.action, meta.issue.number ); 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").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").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 .join(&format!( "{}/{}", meta.issue.repository.owner, meta.issue.repository.name )) .with_context(|| { format!("failed building repository URL from GITHUB_SERVER_URL: {server_url}") })?; let repo_auth_url = token .as_ref() .map(|token| -> Result<_, ()> { let mut repo_auth_url = repo_url.clone(); repo_auth_url.set_username("forgejo-actions")?; repo_auth_url.set_password(Some(token))?; Ok(repo_auth_url) }) .transpose() .map_err(|_e| anyhow::anyhow!("Repo URL does not have correct format")) .context("Failed adding auth info to repo 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")?; let ctx = Context { forgejo, owner: meta.issue.repository.owner.clone(), repo: meta.issue.repository.name.clone(), repo_url, repo_auth_url, timestamp, }; if meta.action == event_meta::IssueAction::Opened { log::debug!("Running for a newly opened issue. Taking care of the comment first..."); match issue::find_or_make_bot_comment(&ctx, meta.issue.number).await { Ok((_comment, is_new)) => { if is_new { log::info!( "Waiting for a minute, as the issue is brand new. We will likely catch some more updates this way!" ); tokio::time::sleep(std::time::Duration::from_secs(60)).await; } } Err(err) => { log::warn!( "Error while commenting on new issue #{}, continuing anyway... Error: {err:?}", meta.issue.number ); } } } log::info!("Collecting tree from issue metadata..."); let tree = collect::collect_tree(&ctx) .await .context("Failed to collect the techtree from issue metadata")?; log::info!("Rendering and publishing techtree to git repository..."); let rendered_tree = render::render(&ctx, &tree) .await .context("Failed to render the techtree")?; 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 { log::info!("Updating the wiki overview..."); wiki::update_wiki_from_tree(&ctx, &tree) .await .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) .expect("issue from tree not found in tree"); let hash = subtree.stable_hash(); let (bot_comment, _is_new) = issue::find_or_make_bot_comment(&ctx, issue) .await .with_context(|| format!("Failed to find or make bot comment in issue #{issue}"))?; if bot_comment.body.contains(&hash) { log::debug!("Issue #{issue} is up to date, not editing comment."); issue::remove_stale_label(&ctx, issue) .await .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?; continue 'issues; } log::info!("Updating bot comment in issue #{issue} ..."); issue::update_bot_comment_from_subtree(&ctx, bot_comment.id, &subtree, &hash) .await .with_context(|| format!("Failed to update bot comment in issue #{issue}"))?; issue::remove_stale_label(&ctx, issue) .await .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?; } Ok(()) } #[tokio::main] async fn main() -> anyhow::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); run().await }