Compare commits

...

4 commits

Author SHA1 Message Date
Rahix 9519414929 Push all ultimate elements to the bottom of the graph
All checks were successful
/ build (push) Successful in 1m6s
In dot rendering, put all ultimate elements (those that nothing further
depends on) on the same rank, meaning the very bottom.

This helps to visualize our ultimate goals more easily and also shows
elements that are probably a requisite to something else that has not
yet been modelled.
2025-07-28 15:40:05 +02:00
Rahix 1f889245af Make tree serde-serializable 2025-07-28 15:29:26 +02:00
Rahix 92e8ea066f Hint about TECHTREE_FAKE=1 trick
Make it easier to get started with the techtree-manager codebase by
hinting at the TECHTREE_FAKE=1 variable for local development.
2025-07-28 15:29:26 +02:00
Rahix d4b8241808 Allow running the manager without a token
This is useful for local testing.
2025-05-30 21:35:00 +02:00
5 changed files with 64 additions and 17 deletions

View file

@ -964,6 +964,7 @@ dependencies = [
"hashbrown",
"indexmap",
"serde",
"serde_derive",
]
[[package]]

View file

@ -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"

View file

@ -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<url::Url>,
/// 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();

View file

@ -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")?;

View file

@ -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<Element, ()>,
#[serde(skip)]
issue_map: BTreeMap<u64, ElementIndex>,
}
@ -158,6 +160,16 @@ impl Tree {
Subtree::new_for_element(self, element)
}
pub fn iter_ultimate_elements<'a>(&'a self) -> impl Iterator<Item = ElementIndex> + '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}; }}
{:?}
}}
"#,