manager: Factor out a common context object

Clean up the global data a bit by bundling it in a context object.
This commit is contained in:
Rahix 2025-05-22 18:47:31 +02:00
parent eebbd9c453
commit bc54cd91fd
5 changed files with 84 additions and 77 deletions

View file

@ -1,14 +1,12 @@
use anyhow::Context as _; use anyhow::Context as _;
/// Read all issues to generate the full techtree /// Read all issues to generate the full techtree
pub async fn collect_tree( pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result<crate::tree::Tree> {
forgejo: &forgejo_api::Forgejo, let issues = ctx
meta: &crate::event_meta::IssueEventMeta, .forgejo
) -> anyhow::Result<crate::tree::Tree> {
let issues = forgejo
.issue_list_issues( .issue_list_issues(
&meta.issue.repository.owner, &ctx.owner,
&meta.issue.repository.name, &ctx.repo,
forgejo_api::structs::IssueListIssuesQuery { forgejo_api::structs::IssueListIssuesQuery {
// We also want the closed issues // We also want the closed issues
state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), state: Some(forgejo_api::structs::IssueListIssuesQueryState::All),
@ -43,10 +41,12 @@ pub async fn collect_tree(
} }
for issue in issue_numbers.into_iter() { for issue in issue_numbers.into_iter() {
let dependencies = forgejo let dependencies = ctx
.forgejo
.issue_list_issue_dependencies( .issue_list_issue_dependencies(
&meta.issue.repository.owner, &ctx.owner,
&meta.issue.repository.name, &ctx.repo,
// Why the hell is the issue number a string here?
&issue.to_string(), &issue.to_string(),
forgejo_api::structs::IssueListIssueDependenciesQuery { forgejo_api::structs::IssueListIssueDependenciesQuery {
limit: Some(10000), limit: Some(10000),
@ -54,12 +54,7 @@ pub async fn collect_tree(
}, },
) )
.await .await
.with_context(|| { .with_context(|| format!("Failed to fetch issue dependencies for #{issue}",))?;
format!(
"Failed to fetch issue dependencies for #{}",
meta.issue.number
)
})?;
for dep in dependencies { for dep in dependencies {
let dep_number = dep.number.context("Missing issue number in dependency")?; let dep_number = dep.number.context("Missing issue number in dependency")?;

View file

@ -8,17 +8,16 @@ pub struct BotCommentInfo {
} }
pub async fn make_bot_comment( pub async fn make_bot_comment(
forgejo: &forgejo_api::Forgejo, ctx: &crate::Context,
meta: &crate::event_meta::IssueEventMeta,
issue_number: u64, issue_number: u64,
) -> anyhow::Result<CommentId> { ) -> anyhow::Result<CommentId> {
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 = forgejo let res = ctx.forgejo
.issue_create_comment( .issue_create_comment(
&meta.issue.repository.owner, &ctx.owner,
&meta.issue.repository.name, &ctx.repo,
issue_number, issue_number,
forgejo_api::structs::CreateIssueCommentOption { forgejo_api::structs::CreateIssueCommentOption {
body: initial_message.to_owned(), body: initial_message.to_owned(),
@ -31,14 +30,13 @@ pub async fn make_bot_comment(
} }
pub async fn find_bot_comment( pub async fn find_bot_comment(
forgejo: &forgejo_api::Forgejo, ctx: &crate::Context,
meta: &crate::event_meta::RepoMeta,
issue_number: u64, issue_number: u64,
) -> anyhow::Result<Option<BotCommentInfo>> { ) -> anyhow::Result<Option<BotCommentInfo>> {
let mut comments = forgejo let mut comments = ctx.forgejo
.issue_get_comments( .issue_get_comments(
&meta.owner, &ctx.owner,
&meta.name, &ctx.repo,
issue_number, issue_number,
forgejo_api::structs::IssueGetCommentsQuery { forgejo_api::structs::IssueGetCommentsQuery {
..Default::default() ..Default::default()
@ -61,15 +59,14 @@ pub async fn find_bot_comment(
} }
pub async fn update_bot_comment( pub async fn update_bot_comment(
forgejo: &forgejo_api::Forgejo, ctx: &crate::Context,
meta: &crate::event_meta::RepoMeta,
id: CommentId, id: CommentId,
new_body: String, new_body: String,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
forgejo ctx.forgejo
.issue_edit_comment( .issue_edit_comment(
&meta.owner, &ctx.owner,
&meta.name, &ctx.repo,
id, id,
forgejo_api::structs::EditIssueCommentOption { forgejo_api::structs::EditIssueCommentOption {
body: new_body, body: new_body,
@ -83,12 +80,11 @@ pub async fn update_bot_comment(
} }
pub async fn remove_stale_label( pub async fn remove_stale_label(
forgejo: &forgejo_api::Forgejo, ctx: &crate::Context,
meta: &crate::event_meta::RepoMeta,
issue_number: u64, issue_number: u64,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let labels = forgejo let labels = ctx.forgejo
.issue_get_labels(&meta.owner, &meta.name, issue_number) .issue_get_labels(&ctx.owner, &ctx.repo, issue_number)
.await .await
.context("Failed fetching issue labels")?; .context("Failed fetching issue labels")?;
@ -101,10 +97,10 @@ 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 = forgejo let res = ctx.forgejo
.issue_remove_label( .issue_remove_label(
&meta.owner, &ctx.owner,
&meta.name, &ctx.repo,
issue_number, issue_number,
stale_label_id, stale_label_id,
forgejo_api::structs::DeleteLabelsOption { forgejo_api::structs::DeleteLabelsOption {

View file

@ -10,6 +10,21 @@ mod render;
mod tree; mod tree;
mod wiki; 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: url::Url,
/// Human readable timestamp of this manager run
pub timestamp: String,
}
async fn run() -> anyhow::Result<()> { async fn run() -> anyhow::Result<()> {
let meta = if std::env::var("TECHTREE_FAKE").ok().is_some() { let meta = if std::env::var("TECHTREE_FAKE").ok().is_some() {
log::warn!("Fake tree!"); log::warn!("Fake tree!");
@ -48,9 +63,17 @@ async fn run() -> anyhow::Result<()> {
let forgejo = Forgejo::new(auth, server_url).context("Could not create API access object")?; let forgejo = Forgejo::new(auth, server_url).context("Could not create 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,
};
let new_comment_id = if meta.action == event_meta::IssueAction::Opened { let new_comment_id = if meta.action == event_meta::IssueAction::Opened {
let bot_comment = let bot_comment = issue::find_bot_comment(&ctx, meta.issue.number)
issue::find_bot_comment(&forgejo, &meta.issue.repository, meta.issue.number)
.await .await
.with_context(|| { .with_context(|| {
format!( format!(
@ -62,7 +85,7 @@ async fn run() -> anyhow::Result<()> {
let id = match bot_comment { let id = match bot_comment {
Some(comment) => Some(comment.id), Some(comment) => Some(comment.id),
None => { None => {
let res = issue::make_bot_comment(&forgejo, &meta, meta.issue.number).await; let res = issue::make_bot_comment(&ctx, meta.issue.number).await;
match res { match res {
Ok(id) => Some(id), Ok(id) => Some(id),
Err(e) => { Err(e) => {
@ -86,17 +109,17 @@ async fn run() -> anyhow::Result<()> {
None None
}; };
let tree = collect::collect_tree(&forgejo, &meta) 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(&tree, &timestamp, &repo_auth_url) render::render_and_publish(&ctx, &tree)
.await .await
.context("Failed to render and publish the techtree to git")?; .context("Failed to render and publish the techtree to git")?;
if false { if false {
let mermaid = tree.to_mermaid(&repo_url.to_string()); let mermaid = tree.to_mermaid(&ctx.repo_url.to_string());
let wiki_text = format!( let wiki_text = format!(
r##"This page is automatically updated to show the latest and greatest FAFO techtree: r##"This page is automatically updated to show the latest and greatest FAFO techtree:
@ -106,12 +129,7 @@ async fn run() -> anyhow::Result<()> {
"## "##
); );
log::info!("Updating the wiki overview..."); log::info!("Updating the wiki overview...");
wiki::update_wiki_overview( wiki::update_wiki_overview(&ctx, wiki_text)
&forgejo,
&meta.issue.repository,
timestamp.to_string(),
wiki_text,
)
.await .await
.context("Failed to update the techtree wiki page")?; .context("Failed to update the techtree wiki page")?;
} }
@ -123,14 +141,14 @@ async fn run() -> anyhow::Result<()> {
let comment_id = if new_comment_id.is_some() && issue == meta.issue.number { let comment_id = if new_comment_id.is_some() && issue == meta.issue.number {
new_comment_id.unwrap() new_comment_id.unwrap()
} else { } else {
let bot_comment = issue::find_bot_comment(&forgejo, &meta.issue.repository, issue) let bot_comment = issue::find_bot_comment(&ctx, issue)
.await .await
.with_context(|| format!("Failed searching for bot comment for issue #{issue}"))?; .with_context(|| format!("Failed searching for bot comment for issue #{issue}"))?;
if let Some(bot_comment) = bot_comment { if let Some(bot_comment) = bot_comment {
if bot_comment.body.contains(&hash) { if bot_comment.body.contains(&hash) {
log::info!("Issue #{issue} is up-to-date, not editing comment."); log::info!("Issue #{issue} is up-to-date, not editing comment.");
issue::remove_stale_label(&forgejo, &meta.issue.repository, issue) issue::remove_stale_label(&ctx, issue)
.await .await
.with_context(|| { .with_context(|| {
format!("Failed to remove `Stale` label from issue #{issue}") format!("Failed to remove `Stale` label from issue #{issue}")
@ -142,7 +160,7 @@ async fn run() -> anyhow::Result<()> {
} else { } else {
log::warn!("Missing bot comment in issue #{issue}"); log::warn!("Missing bot comment in issue #{issue}");
issue::make_bot_comment(&forgejo, &meta, issue) issue::make_bot_comment(&ctx, issue)
.await .await
.with_context(|| { .with_context(|| {
format!("Failed to create a retrospective bot comment on issue #{issue}") format!("Failed to create a retrospective bot comment on issue #{issue}")
@ -150,7 +168,7 @@ async fn run() -> anyhow::Result<()> {
} }
}; };
let mermaid = subtree.to_mermaid(&repo_url.to_string()); let mermaid = subtree.to_mermaid(&ctx.repo_url.to_string());
let full_text = format!( let full_text = format!(
r##"## Partial Techtree r##"## Partial Techtree
@ -158,15 +176,16 @@ async fn run() -> anyhow::Result<()> {
{mermaid} {mermaid}
``` ```
<small>Digest: {hash}; Last Updated: {timestamp}</small>"## <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(&forgejo, &meta.issue.repository, comment_id, full_text) issue::update_bot_comment(&ctx, comment_id, full_text)
.await .await
.with_context(|| format!("Failed to update the bot comment in issue #{issue}"))?; .with_context(|| format!("Failed to update the bot comment in issue #{issue}"))?;
issue::remove_stale_label(&forgejo, &meta.issue.repository, issue) issue::remove_stale_label(&ctx, issue)
.await .await
.with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?; .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?;
} }

View file

@ -16,9 +16,8 @@ impl SuccessExt for Command {
} }
pub async fn render_and_publish( pub async fn render_and_publish(
ctx: &crate::Context,
tree: &crate::tree::Tree, tree: &crate::tree::Tree,
timestamp: &str,
repo_auth_url: &url::Url,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let render_repo = std::path::PathBuf::from("render-git"); let render_repo = std::path::PathBuf::from("render-git");
@ -81,7 +80,7 @@ pub async fn render_and_publish(
.arg("-C") .arg("-C")
.arg(&render_repo) .arg(&render_repo)
.arg("commit") .arg("commit")
.args(["-m", &format!("Updated techtree at {timestamp}")]) .args(["-m", &format!("Updated techtree at {}", ctx.timestamp)])
.success() .success()
.context("Failed to add generated graph files to git index")?; .context("Failed to add generated graph files to git index")?;
@ -91,7 +90,7 @@ pub async fn render_and_publish(
.arg("push") .arg("push")
.arg("--force") .arg("--force")
.arg("--quiet") .arg("--quiet")
.arg(repo_auth_url.to_string()) .arg(ctx.repo_auth_url.to_string())
.arg("HEAD:refs/heads/render") .arg("HEAD:refs/heads/render")
.status() .status()
.context("Failed to push rendered graph to forgejo repository")?; .context("Failed to push rendered graph to forgejo repository")?;

View file

@ -2,20 +2,18 @@ use anyhow::Context as _;
use base64::prelude::*; use base64::prelude::*;
pub async fn update_wiki_overview( pub async fn update_wiki_overview(
forgejo: &forgejo_api::Forgejo, ctx: &crate::Context,
meta: &crate::event_meta::RepoMeta,
timestamp: String,
new_body: String, new_body: String,
) -> anyhow::Result<()> { ) -> 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 _ = forgejo let _ = ctx.forgejo
.repo_edit_wiki_page( .repo_edit_wiki_page(
&meta.owner, &ctx.owner,
&meta.name, &ctx.repo,
"Home", "Home",
forgejo_api::structs::CreateWikiPageOptions { forgejo_api::structs::CreateWikiPageOptions {
content_base64: Some(BASE64_STANDARD.encode(new_body.as_bytes())), content_base64: Some(BASE64_STANDARD.encode(new_body.as_bytes())),
message: Some(format!("Updated to latest model at {timestamp}")), message: Some(format!("Updated to latest model at {}", ctx.timestamp)),
title: Some("Home".to_owned()), title: Some("Home".to_owned()),
}, },
) )