manager: The big refactoring

Clean up the logic in main to make it more readable.  Factor out common
functionality into the respective modules.
This commit is contained in:
Rahix 2025-05-22 20:27:08 +02:00
parent bc54cd91fd
commit 785ff37107
5 changed files with 171 additions and 140 deletions

View file

@ -10,11 +10,12 @@ pub struct BotCommentInfo {
pub async fn make_bot_comment( pub async fn make_bot_comment(
ctx: &crate::Context, ctx: &crate::Context,
issue_number: u64, issue_number: u64,
) -> anyhow::Result<CommentId> { ) -> anyhow::Result<BotCommentInfo> {
let initial_message = let initial_message =
"_Please be patient, this issue is currently being integrated into the techtree..._"; "_Please be patient, this issue is currently being integrated into the techtree..._";
let res = ctx.forgejo let res = ctx
.forgejo
.issue_create_comment( .issue_create_comment(
&ctx.owner, &ctx.owner,
&ctx.repo, &ctx.repo,
@ -26,14 +27,18 @@ pub async fn make_bot_comment(
) )
.await?; .await?;
Ok(res.id.unwrap()) Ok(BotCommentInfo {
id: res.id.unwrap(),
body: initial_message.to_owned(),
})
} }
pub async fn find_bot_comment( pub async fn find_bot_comment(
ctx: &crate::Context, ctx: &crate::Context,
issue_number: u64, issue_number: u64,
) -> anyhow::Result<Option<BotCommentInfo>> { ) -> anyhow::Result<Option<BotCommentInfo>> {
let mut comments = ctx.forgejo let mut comments = ctx
.forgejo
.issue_get_comments( .issue_get_comments(
&ctx.owner, &ctx.owner,
&ctx.repo, &ctx.repo,
@ -58,6 +63,27 @@ pub async fn find_bot_comment(
})) }))
} }
/// Find existing bot comment or create a new one.
///
/// Returns a tuple of the comment information and a boolean indicating whether the comment was
/// newly created.
pub async fn find_or_make_bot_comment(
ctx: &crate::Context,
issue_number: u64,
) -> anyhow::Result<(BotCommentInfo, bool)> {
if let Some(comment) = find_bot_comment(ctx, issue_number)
.await
.context("Failed to search for bot comment in issue")?
{
Ok((comment, false))
} else {
make_bot_comment(ctx, issue_number)
.await
.context("Failed to make new bot comment in issue")
.map(|c| (c, true))
}
}
pub async fn update_bot_comment( pub async fn update_bot_comment(
ctx: &crate::Context, ctx: &crate::Context,
id: CommentId, id: CommentId,
@ -79,11 +105,32 @@ pub async fn update_bot_comment(
Ok(()) Ok(())
} }
pub async fn remove_stale_label( pub async fn update_bot_comment_from_subtree(
ctx: &crate::Context, ctx: &crate::Context,
issue_number: u64, id: CommentId,
subtree: &crate::tree::Subtree<'_>,
hash: &str,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let labels = ctx.forgejo let mermaid = subtree.to_mermaid(&ctx.repo_url.to_string());
let full_text = format!(
r##"## Partial Techtree
```mermaid
{mermaid}
```
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
timestamp = ctx.timestamp,
);
update_bot_comment(&ctx, id, full_text).await?;
Ok(())
}
pub async fn remove_stale_label(ctx: &crate::Context, issue_number: u64) -> anyhow::Result<()> {
let labels = ctx
.forgejo
.issue_get_labels(&ctx.owner, &ctx.repo, issue_number) .issue_get_labels(&ctx.owner, &ctx.repo, issue_number)
.await .await
.context("Failed fetching issue labels")?; .context("Failed fetching issue labels")?;
@ -97,7 +144,8 @@ pub async fn remove_stale_label(
if let Some(stale_label_id) = stale_label_id { if let Some(stale_label_id) = stale_label_id {
log::info!("Removing `Stale` label from issue #{issue_number}..."); log::info!("Removing `Stale` label from issue #{issue_number}...");
let res = ctx.forgejo let res = ctx
.forgejo
.issue_remove_label( .issue_remove_label(
&ctx.owner, &ctx.owner,
&ctx.repo, &ctx.repo,

View file

@ -72,64 +72,43 @@ async fn run() -> anyhow::Result<()> {
timestamp, timestamp,
}; };
let new_comment_id = if meta.action == event_meta::IssueAction::Opened { if meta.action == event_meta::IssueAction::Opened {
let bot_comment = issue::find_bot_comment(&ctx, meta.issue.number) log::debug!("Running for a newly opened issue. Taking care of the comment first...");
.await match issue::find_or_make_bot_comment(&ctx, meta.issue.number).await {
.with_context(|| { Ok((_comment, is_new)) => {
format!( if is_new {
"Failed searching for bot comment for issue #{}", log::info!(
meta.issue.number "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;
let id = match bot_comment {
Some(comment) => Some(comment.id),
None => {
let res = issue::make_bot_comment(&ctx, meta.issue.number).await;
match res {
Ok(id) => Some(id),
Err(e) => {
log::warn!(
"Error while creating the informational comment on issue #{}:\n{e:?}",
meta.issue.number
);
None
}
} }
} }
}; Err(err) => {
log::warn!(
log::info!( "Error while commenting on new issue #{}, continuing anyway... Error: {err:?}",
"Waiting for a minute, as the issue is brand new. We will likely catch some more updates this way!" meta.issue.number
); );
tokio::time::sleep(std::time::Duration::from_secs(60)).await; }
}
id }
} else {
None
};
log::info!("Collecting tree from issue metadata...");
let tree = collect::collect_tree(&ctx) let tree = collect::collect_tree(&ctx)
.await .await
.context("Failed to collect the techtree from issue metadata")?; .context("Failed to collect the techtree from issue metadata")?;
log::info!("Rendering and publishing techtree to git repository..."); log::info!("Rendering and publishing techtree to git repository...");
render::render_and_publish(&ctx, &tree) let rendered_tree = render::render(&ctx, &tree)
.await .await
.context("Failed to render and publish the techtree to git")?; .context("Failed to render the techtree")?;
render::publish(&ctx, &rendered_tree)
.await
.context("Failed to publish rendered tree to git")?;
// Wiki is disabled because the tree is too big for mermaid to handle
if false { if false {
let mermaid = tree.to_mermaid(&ctx.repo_url.to_string());
let wiki_text = format!(
r##"This page is automatically updated to show the latest and greatest FAFO techtree:
```mermaid
{mermaid}
```
"##
);
log::info!("Updating the wiki overview..."); log::info!("Updating the wiki overview...");
wiki::update_wiki_overview(&ctx, wiki_text) wiki::update_wiki_from_tree(&ctx, &tree)
.await .await
.context("Failed to update the techtree wiki page")?; .context("Failed to update the techtree wiki page")?;
} }
@ -138,52 +117,23 @@ async fn run() -> anyhow::Result<()> {
let subtree = tree.subtree_for_issue(issue).unwrap(); let subtree = tree.subtree_for_issue(issue).unwrap();
let hash = subtree.stable_hash(); let hash = subtree.stable_hash();
let comment_id = if new_comment_id.is_some() && issue == meta.issue.number { let (bot_comment, _is_new) = issue::find_or_make_bot_comment(&ctx, issue)
new_comment_id.unwrap() .await
} else { .with_context(|| format!("Failed to find or make bot comment in issue #{issue}"))?;
let bot_comment = issue::find_bot_comment(&ctx, issue)
if bot_comment.body.contains(&hash) {
log::info!("Issue #{issue} is up-to-date, not editing comment.");
issue::remove_stale_label(&ctx, issue)
.await .await
.with_context(|| format!("Failed searching for bot comment for issue #{issue}"))?; .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?;
if let Some(bot_comment) = bot_comment { continue 'issues;
if bot_comment.body.contains(&hash) { }
log::info!("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;
}
bot_comment.id
} else {
log::warn!("Missing bot comment in issue #{issue}");
issue::make_bot_comment(&ctx, issue)
.await
.with_context(|| {
format!("Failed to create a retrospective bot comment on issue #{issue}")
})?
}
};
let mermaid = subtree.to_mermaid(&ctx.repo_url.to_string());
let full_text = format!(
r##"## Partial Techtree
```mermaid
{mermaid}
```
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
timestamp = ctx.timestamp,
);
log::info!("Updating bot comment in issue #{issue} ..."); log::info!("Updating bot comment in issue #{issue} ...");
issue::update_bot_comment(&ctx, comment_id, full_text) issue::update_bot_comment_from_subtree(&ctx, bot_comment.id, &subtree, &hash)
.await .await
.with_context(|| format!("Failed to update the bot comment in issue #{issue}"))?; .with_context(|| format!("Failed to update bot comment in issue #{issue}"))?;
issue::remove_stale_label(&ctx, issue) issue::remove_stale_label(&ctx, issue)
.await .await

View file

@ -15,45 +15,27 @@ impl SuccessExt for Command {
} }
} }
pub async fn render_and_publish( pub struct RenderedTree {
ctx: &crate::Context, repo_dir: std::path::PathBuf,
tree: &crate::tree::Tree, dot_file: std::path::PathBuf,
) -> anyhow::Result<()> { svg_file: std::path::PathBuf,
let render_repo = std::path::PathBuf::from("render-git"); }
if render_repo.is_dir() { pub async fn render(
log::info!("Found old {render_repo:?} repository, removing..."); _ctx: &crate::Context,
std::fs::remove_dir_all(&render_repo) tree: &crate::tree::Tree,
.context("Failed to remove stale render repository")?; ) -> anyhow::Result<RenderedTree> {
let repo_dir = std::path::PathBuf::from("render-git");
if repo_dir.is_dir() {
log::info!("Found old {repo_dir:?} repository, removing...");
std::fs::remove_dir_all(&repo_dir).context("Failed to remove stale render repository")?;
} }
std::fs::create_dir(&render_repo).context("Failed creating directory for rendered graph")?; std::fs::create_dir(&repo_dir).context("Failed creating directory for rendered graph")?;
Command::new("git") let dot_file = repo_dir.join("techtree.dot");
.arg("-C") let svg_file = repo_dir.join("techtree.svg");
.arg(&render_repo)
.arg("init")
.arg("--initial-branch=render")
.arg("--quiet")
.success()
.context("Failed to initialize render repository")?;
Command::new("git")
.arg("-C")
.arg(&render_repo)
.args(["config", "user.email", "git@fa-fo.de"])
.success()
.context("Failed to configure identity for render repo")?;
Command::new("git")
.arg("-C")
.arg(&render_repo)
.args(["config", "user.name", "Forgejo Actions"])
.success()
.context("Failed to configure identity for render repo")?;
let dot_file = render_repo.join("techtree.dot");
let svg_file = render_repo.join("techtree.svg");
let dot_source = tree.to_dot(); let dot_source = tree.to_dot();
std::fs::write(&dot_file, dot_source.as_bytes()) std::fs::write(&dot_file, dot_source.as_bytes())
@ -67,18 +49,49 @@ pub async fn render_and_publish(
.success() .success()
.context("Failed to generate svg graph from dot source")?; .context("Failed to generate svg graph from dot source")?;
Ok(RenderedTree {
repo_dir,
dot_file,
svg_file,
})
}
pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::Result<()> {
Command::new("git") Command::new("git")
.arg("-C") .arg("-C")
.arg(&render_repo) .arg(&rendered.repo_dir)
.arg("init")
.arg("--initial-branch=render")
.arg("--quiet")
.success()
.context("Failed to initialize render repository")?;
Command::new("git")
.arg("-C")
.arg(&rendered.repo_dir)
.args(["config", "user.email", "git@fa-fo.de"])
.success()
.context("Failed to configure identity for render repo")?;
Command::new("git")
.arg("-C")
.arg(&rendered.repo_dir)
.args(["config", "user.name", "Forgejo Actions"])
.success()
.context("Failed to configure identity for render repo")?;
Command::new("git")
.arg("-C")
.arg(&rendered.repo_dir)
.arg("add") .arg("add")
.arg(dot_file.file_name().unwrap()) .arg(rendered.dot_file.strip_prefix(&rendered.repo_dir).unwrap())
.arg(svg_file.file_name().unwrap()) .arg(rendered.svg_file.strip_prefix(&rendered.repo_dir).unwrap())
.success() .success()
.context("Failed to add generated graph files to git index")?; .context("Failed to add generated graph files to git index")?;
Command::new("git") Command::new("git")
.arg("-C") .arg("-C")
.arg(&render_repo) .arg(&rendered.repo_dir)
.arg("commit") .arg("commit")
.args(["-m", &format!("Updated techtree at {}", ctx.timestamp)]) .args(["-m", &format!("Updated techtree at {}", ctx.timestamp)])
.success() .success()
@ -86,7 +99,7 @@ pub async fn render_and_publish(
Command::new("git") Command::new("git")
.arg("-C") .arg("-C")
.arg(&render_repo) .arg(&rendered.repo_dir)
.arg("push") .arg("push")
.arg("--force") .arg("--force")
.arg("--quiet") .arg("--quiet")

View file

@ -166,11 +166,14 @@ impl Tree {
&|_g, (_, element)| element.to_dot_node_attributes(None), &|_g, (_, element)| element.to_dot_node_attributes(None),
); );
format!(r#"digraph {{ format!(
r#"digraph {{
ranksep=1.2 ranksep=1.2
{:?} {:?}
}} }}
"#, dot) "#,
dot
)
} }
pub fn to_mermaid(&self, repo_url: &str) -> String { pub fn to_mermaid(&self, repo_url: &str) -> String {

View file

@ -1,12 +1,29 @@
use anyhow::Context as _; use anyhow::Context as _;
use base64::prelude::*; use base64::prelude::*;
pub async fn update_wiki_overview( pub async fn update_wiki_from_tree(
ctx: &crate::Context, ctx: &crate::Context,
new_body: String, tree: &crate::tree::Tree,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mermaid = tree.to_mermaid(&ctx.repo_url.to_string());
let wiki_text = format!(
r##"This page is automatically updated to show the latest and greatest FAFO techtree:
```mermaid
{mermaid}
```
"##
);
update_wiki_overview(&ctx, wiki_text)
.await
.context("Failed to update the techtree wiki page")?;
Ok(())
}
pub async fn update_wiki_overview(ctx: &crate::Context, new_body: String) -> anyhow::Result<()> {
// TODO: Figure out why we get a 404 when the edit was successfull... // TODO: Figure out why we get a 404 when the edit was successfull...
let _ = ctx.forgejo let _ = ctx
.forgejo
.repo_edit_wiki_page( .repo_edit_wiki_page(
&ctx.owner, &ctx.owner,
&ctx.repo, &ctx.repo,