From 8ab73f4a4a756a9fb870c39353c453dbc2df454b Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 17 May 2025 01:37:23 +0200 Subject: [PATCH 01/63] tree: init from IC RE talk --- README.md | 6 ++++++ tree.dot | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 README.md create mode 100644 tree.dot diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d60249 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +GUFTT +===== + +Grand Unified FAFO Tech Tree + +Work in progress during FOF1. diff --git a/tree.dot b/tree.dot new file mode 100644 index 0000000..906d6d1 --- /dev/null +++ b/tree.dot @@ -0,0 +1,52 @@ +digraph { + wet_lab [label="Wet Lab"] + lapping [label="Lapping Machine"] + fiber_laser [label="Fiber Laser"] + rie [label="Reactive Ion Etching"] + plasma [label="Plasma Etching"] + + wet_lab -> decapping [label="HNO3/H2SO4"] + lapping -> decapping + fiber_laser -> decapping + plasma -> decapping + decapping [label="Decapping"] + + delayering [label="Delayering"] + fiber_laser -> delayering + lapping -> delayering + rie -> delayering + decapping -> delayering + wet_lab -> delayering [label="HNO3/HF"] + + optical_stage [label="Optical Microscope with X/Y motorized stage"] + + laser_fault_injection [label="Laser Fault Injection"] + decapping -> laser_fault_injection + optical_stage -> laser_fault_injection + + body_bias_injection [label="Body Bias Injection"] + decapping -> body_bias_injection + + sem_motorized_stage [label="SEM motorized stage"] + + chip_imaging [label="Simple Chip Imaging"] + delayering -> chip_imaging + sem_motorized_stage -> chip_imaging + + nanoprobing [label="SEM nanoprobing/live analysis"] + sem_motorized_stage -> nanoprobing + delayering -> nanoprobing + + sem_fault_injection [label="SEM Fault Injection"] + nanoprobing -> sem_fault_injection + + staining [label="DASH Stain"] + wet_lab -> staining [label="HNO3/HF"] + + full_chip_re [label="Full Chip RE"] + chip_imaging -> full_chip_re + staining -> full_chip_re + + microprobing [label="Microprobing"] + optical_stage -> microprobing +} From 870e54e26312915cf3088a3d32f19f04ea28fce4 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 09:48:01 +0200 Subject: [PATCH 02/63] manager: Add first version of the techtree manager The techtree manager is a CI tool that derives the techtree graph from the forgejo issues in this repository. It then adds graph visualizations to each issue and the wiki, to give an overview of the tree. --- techtree-manager/.gitignore | 1 + techtree-manager/Cargo.lock | 1986 ++++++++++++++++++++++++++++ techtree-manager/Cargo.toml | 18 + techtree-manager/src/collect.rs | 132 ++ techtree-manager/src/event_meta.rs | 57 + techtree-manager/src/issue.rs | 82 ++ techtree-manager/src/main.rs | 125 ++ techtree-manager/src/tree.rs | 343 +++++ techtree-manager/src/wiki.rs | 24 + 9 files changed, 2768 insertions(+) create mode 100644 techtree-manager/.gitignore create mode 100644 techtree-manager/Cargo.lock create mode 100644 techtree-manager/Cargo.toml create mode 100644 techtree-manager/src/collect.rs create mode 100644 techtree-manager/src/event_meta.rs create mode 100644 techtree-manager/src/issue.rs create mode 100644 techtree-manager/src/main.rs create mode 100644 techtree-manager/src/tree.rs create mode 100644 techtree-manager/src/wiki.rs diff --git a/techtree-manager/.gitignore b/techtree-manager/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/techtree-manager/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/techtree-manager/Cargo.lock b/techtree-manager/Cargo.lock new file mode 100644 index 0000000..614a664 --- /dev/null +++ b/techtree-manager/Cargo.lock @@ -0,0 +1,1986 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "forgejo-api" +version = "0.3.2" +source = "git+https://git.fa-fo.de/rahix/forgejo-api.git?rev=a3f6452cfe774898a89ac66be393e5205f5e12b7#a3f6452cfe774898a89ac66be393e5205f5e12b7" +dependencies = [ + "base64ct", + "bytes", + "reqwest", + "serde", + "serde_json", + "soft_assert", + "thiserror", + "time", + "tokio", + "url", + "zeroize", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06" +dependencies = [ + "fixedbitset", + "hashbrown", + "indexmap", + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha256" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soft_assert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5097ec7ea7218135541ad96348f1441d0c616537dd4ed9c47205920c35d7d97" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "techtree-manager" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "env_logger", + "forgejo-api", + "log", + "petgraph", + "serde", + "serde_json", + "sha256", + "tokio", + "url", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/techtree-manager/Cargo.toml b/techtree-manager/Cargo.toml new file mode 100644 index 0000000..afbfdcd --- /dev/null +++ b/techtree-manager/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "techtree-manager" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +base64 = "0.22.1" +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" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +sha256 = "1.6.0" +tokio = { version = "1.45.0", features = ["full"] } +url = "2.5.4" diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs new file mode 100644 index 0000000..4b06f6e --- /dev/null +++ b/techtree-manager/src/collect.rs @@ -0,0 +1,132 @@ +use anyhow::Context as _; +use std::collections::BTreeMap; + +/// Read all issues to generate the full techtree +pub async fn collect_tree( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::IssueEventMeta, +) -> anyhow::Result { + let issues = forgejo + .issue_list_issues( + &meta.issue.repository.owner, + &meta.issue.repository.name, + forgejo_api::structs::IssueListIssuesQuery { + // We also want the closed issues + state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), + // No pagination + limit: None, + // Only issues + r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues), + ..Default::default() + }, + ) + .await + .context("Failed fetching issue list")?; + + let mut tree = crate::tree::Tree::new(); + let mut issue_numbers = Vec::new(); + + for issue in issues.iter() { + let element = match element_from_issue(issue) { + Ok(el) => el, + Err(e) => { + let maybe_number = issue + .number + .map(|n| n.to_string()) + .unwrap_or("".to_owned()); + log::warn!("Failed processing issue #{maybe_number}: {e:?}"); + continue; + } + }; + + issue_numbers.push(element.issue_number); + tree.add_element(element); + } + + for issue in issue_numbers.into_iter() { + let dependencies = forgejo + .issue_list_issue_dependencies( + &meta.issue.repository.owner, + &meta.issue.repository.name, + &issue.to_string(), + forgejo_api::structs::IssueListIssueDependenciesQuery { + limit: None, + ..Default::default() + }, + ) + .await + .with_context(|| { + format!( + "Failed to fetch issue dependencies for #{}", + meta.issue.number + ) + })?; + + for dep in dependencies { + let dep_number = dep.number.context("Missing issue number in dependency")?; + if !tree.find_element_by_issue_number(dep_number).is_some() { + log::warn!("Found dependency from #{issue} on non-tracked issue #{dep_number}!"); + } + + tree.add_dependency_by_issue_number(issue, dep_number); + } + } + + Ok(tree) +} + +fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result { + let issue_number = issue.number.context("Missing issue number")?; + let description = issue + .title + .as_deref() + .context("Issue is missing a title")? + .to_owned(); + + let ty_labels: Vec<_> = issue + .labels + .as_ref() + .context("Issue does not have any labels")? + .iter() + .filter_map(|l| l.name.as_deref()) + .filter(|l| l.starts_with("ty/")) + .collect(); + + if ty_labels.len() == 0 { + anyhow::bail!("Issue #{issue_number} has no type label!"); + } + if ty_labels.len() > 1 { + anyhow::bail!("Issue #{issue_number} has more than one type label!"); + } + + let ty = match *ty_labels.first().unwrap() { + "ty/equipment" => crate::tree::ElementType::Equipment, + "ty/process" => crate::tree::ElementType::Process, + "ty/knowledge" => crate::tree::ElementType::Knowledge, + t => anyhow::bail!("Unknown element type for issue #{issue_number}: {t:?}"), + }; + + let status = match issue.state.context("Missing issue state")? { + forgejo_api::structs::StateType::Open => { + if issue.assignee.is_some() + || issue + .assignees + .as_ref() + .map(|v| v.len() > 0) + .unwrap_or(false) + { + crate::tree::ElementStatus::Assigned + } else { + crate::tree::ElementStatus::Missing + } + } + forgejo_api::structs::StateType::Closed => crate::tree::ElementStatus::Completed, + }; + + Ok(crate::tree::Element { + issue_number, + description, + ty, + status, + }) +} diff --git a/techtree-manager/src/event_meta.rs b/techtree-manager/src/event_meta.rs new file mode 100644 index 0000000..c6aafe6 --- /dev/null +++ b/techtree-manager/src/event_meta.rs @@ -0,0 +1,57 @@ +use anyhow::Context as _; + +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct IssueEventMeta { + pub action: IssueAction, + pub issue: IssueMeta, +} + +#[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum IssueAction { + Opened, + Reopened, + Closed, + Assigned, + Unassigned, + Edited, + #[serde(rename = "label_updated")] + LabelUpdated, + Labeled, + #[serde(rename = "label_cleared")] + LabelCleared, + Unlabeled, +} + +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct IssueMeta { + pub number: u64, + pub repository: RepoMeta, +} + +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct RepoMeta { + pub name: String, + pub owner: String, +} + +pub fn get_issue_event_meta_from_env() -> anyhow::Result { + let path = std::env::var_os("GITHUB_EVENT_PATH") + .context("Could not get event description file path (GITHUB_EVENT_PATH)")?; + let f = std::fs::File::open(path).context("Could not open GITHUB_EVENT_PATH file")?; + let meta: IssueEventMeta = serde_json::de::from_reader(f).context("Failed to parse")?; + Ok(meta) +} + +pub fn fake() -> IssueEventMeta { + IssueEventMeta { + action: IssueAction::Edited, + issue: IssueMeta { + number: 1337, + repository: RepoMeta { + name: "techtree-poc".to_owned(), + owner: "rahix".to_owned(), + }, + }, + } +} diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs new file mode 100644 index 0000000..7f1498c --- /dev/null +++ b/techtree-manager/src/issue.rs @@ -0,0 +1,82 @@ +use anyhow::Context as _; + +pub type CommentId = u64; + +pub struct BotCommentInfo { + pub body: String, + pub id: CommentId, +} + +pub async fn make_bot_comment( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::IssueEventMeta, + issue_number: u64, +) -> anyhow::Result { + let initial_message = + "_Please be patient, this issue is currently being integrated into the techtree..._"; + + let res = forgejo + .issue_create_comment( + &meta.issue.repository.owner, + &meta.issue.repository.name, + issue_number, + forgejo_api::structs::CreateIssueCommentOption { + body: initial_message.to_owned(), + updated_at: None, + }, + ) + .await?; + + Ok(res.id.unwrap()) +} + +pub async fn find_bot_comment( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::RepoMeta, + issue_number: u64, +) -> anyhow::Result> { + let mut comments = forgejo + .issue_get_comments( + &meta.owner, + &meta.name, + issue_number, + forgejo_api::structs::IssueGetCommentsQuery { + ..Default::default() + }, + ) + .await + .context("Failed fetching comments for issue")?; + + comments.sort_by_key(|comment| comment.created_at); + + let maybe_bot_comment = comments + .iter() + .rev() + .find(|comment| comment.user.as_ref().unwrap().id == Some(-2)); + + Ok(maybe_bot_comment.map(|c| BotCommentInfo { + body: c.body.clone().unwrap_or("".to_owned()), + id: c.id.unwrap(), + })) +} + +pub async fn update_bot_comment( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::RepoMeta, + id: CommentId, + new_body: String, +) -> anyhow::Result<()> { + forgejo + .issue_edit_comment( + &meta.owner, + &meta.name, + id, + forgejo_api::structs::EditIssueCommentOption { + body: new_body, + updated_at: None, + }, + ) + .await + .context("Failed to update comment body")?; + Ok(()) +} diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs new file mode 100644 index 0000000..d5933ce --- /dev/null +++ b/techtree-manager/src/main.rs @@ -0,0 +1,125 @@ +use anyhow::Context as _; +use forgejo_api::Forgejo; + +mod collect; +mod event_meta; +mod issue; +mod tree; +mod wiki; + +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("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"); + log::info!("Timestamp of this run is {timestamp}"); + + let token = + std::env::var("GITHUB_TOKEN").context("Failed accessing GITHUB_TOKEN auth token")?; + let auth = forgejo_api::Auth::Token(&token); + let server_url = url::Url::parse( + &std::env::var("GITHUB_SERVER_URL") + .context("Failed reading GITHUB_SERVER_URL server url")?, + ) + .context("Failed parsing GITHUB_SERVER_URL as a url")?; + + let forgejo = Forgejo::new(auth, server_url).context("Could not create API access object")?; + + let new_comment_id = if meta.action == event_meta::IssueAction::Opened { + let res = issue::make_bot_comment(&forgejo, &meta, 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 + } + } + } else { + None + }; + + let tree = collect::collect_tree(&forgejo, &meta) + .await + .context("Failed to collect the techtree from issue metadata")?; + + let mermaid = tree.to_mermaid(); + 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..."); + wiki::update_wiki_overview(&forgejo, &meta.issue.repository, timestamp.to_string(), wiki_text) + .await + .context("Failed to update the techtree wiki page")?; + + 'issues: for issue in tree.iter_issues() { + let subtree = tree.subtree_for_issue(issue).unwrap(); + let hash = subtree.stable_hash(); + + let comment_id = if new_comment_id.is_some() && issue == meta.issue.number { + new_comment_id.unwrap() + } else { + let bot_comment = issue::find_bot_comment(&forgejo, &meta.issue.repository, issue) + .await + .with_context(|| format!("Failed searching for bot comment for issue #{issue}"))?; + + if let Some(bot_comment) = bot_comment { + if bot_comment.body.contains(&hash) { + log::info!("Issue #{issue} is up-to-date, not editing comment."); + continue 'issues; + } + bot_comment.id + } else { + log::warn!("Missing bot comment in issue #{issue}"); + + issue::make_bot_comment(&forgejo, &meta, issue) + .await + .with_context(|| { + format!("Failed to create a retrospective bot comment on issue #{issue}") + })? + } + }; + + let mermaid = subtree.to_mermaid(); + + let full_text = format!( + r##"## Partial Techtree +```mermaid +{mermaid} +``` + +Digest: {hash}; Last Updated: {timestamp}"## + ); + + log::info!("Updating bot comment in issue #{issue} ..."); + issue::update_bot_comment(&forgejo, &meta.issue.repository, comment_id, full_text) + .await + .with_context(|| format!("Failed to update the bot comment in 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 +} diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs new file mode 100644 index 0000000..44b8be0 --- /dev/null +++ b/techtree-manager/src/tree.rs @@ -0,0 +1,343 @@ +use std::collections::BTreeMap; + +use petgraph::visit::EdgeRef as _; + +/// Element in the techtree +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] +pub struct Element { + /// Issue associated with this element + pub issue_number: u64, + /// Description of this element + pub description: String, + /// Type of this element + pub ty: ElementType, + /// Completion status of this element. + pub status: ElementStatus, +} + +impl Element { + fn to_dot_node_attributes(&self, role: Option) -> String { + let Element { + issue_number, + description, + ty, + status, + } = self; + + let mut attributes = Vec::new(); + + attributes.push(format!( + r##"label = "{{{{#{issue_number} | {status}}}|{ty}|{description}}}""## + )); + attributes.push(r#"shape = "record""#.to_owned()); + + let color = match (role, status) { + (Some(SubtreeElementRole::ElementOfInterest), _) => "black", + (Some(SubtreeElementRole::Dependant), _) => "gray", + (_, ElementStatus::Missing) => "darkred", + (_, ElementStatus::Assigned) => "orange", + (_, ElementStatus::Completed) => "darkgreen", + }; + attributes.push(format!(r#"color = "{color}""#)); + attributes.push(format!(r#"fontcolor = "{color}""#)); + + attributes.join(", ") + } + + fn to_mermaid_node(&self, index: ElementIndex, role: Option) -> String { + let Element { + issue_number, + description, + ty, + status, + } = self; + + let label = format!(r##"#{issue_number} | {status}\n{ty}\n{description}"##); + + let class = match (role, status) { + (Some(SubtreeElementRole::ElementOfInterest), _) => "eoi", + (Some(SubtreeElementRole::Dependant), _) => "dependant", + (_, ElementStatus::Missing) => "dep_missing", + (_, ElementStatus::Assigned) => "dep_assigned", + (_, ElementStatus::Completed) => "dep_completed", + }; + + format!( + " {index}:::{class}\n {index}[\"{label}\"]", + index = index.index(), + ) + } +} + +fn mermaid_classes() -> String { + r##" + classDef eoi fill:#fff, stroke:#000; + classDef dependant fill:#fff, stroke:#888, color:#888; + classDef dep_missing fill:#fcc, stroke:#800; + classDef dep_assigned fill:#ffa, stroke:#a50; + classDef dep_completed fill:#afa, stroke:#080; +"## + .to_owned() +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)] +pub enum ElementType { + Equipment, + Process, + Knowledge, +} + +impl std::fmt::Display for ElementType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ElementType::Equipment => "Equipment", + ElementType::Process => "Process", + ElementType::Knowledge => "Knowledge", + }) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)] +pub enum ElementStatus { + Missing, + Assigned, + Completed, +} + +impl std::fmt::Display for ElementStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ElementStatus::Missing => "MISSING", + ElementStatus::Assigned => "ASSIGNED", + ElementStatus::Completed => "COMPLETED", + }) + } +} + +pub type ElementIndex = petgraph::graph::NodeIndex; + +pub struct Tree { + graph: petgraph::Graph, + issue_map: BTreeMap, +} + +impl Tree { + pub fn new() -> Self { + Self { + graph: petgraph::Graph::new(), + issue_map: BTreeMap::new(), + } + } + + pub fn add_element(&mut self, element: Element) { + let issue_number = element.issue_number; + assert!(!self.issue_map.contains_key(&issue_number)); + let idx = self.graph.add_node(element); + self.issue_map.insert(issue_number, idx); + } + + pub fn add_dependency_by_issue_number(&mut self, dependant: u64, dependency: u64) { + let a = self.find_element_by_issue_number(dependant).unwrap(); + let b = self.find_element_by_issue_number(dependency).unwrap(); + self.graph.add_edge(a, b, ()); + } + + pub fn find_element_by_issue_number(&self, issue_number: u64) -> Option { + self.issue_map.get(&issue_number).copied() + } + + pub fn subtree_for_issue<'a>(&'a self, issue_number: u64) -> Option> { + Some(Subtree::new_for_element( + self, + self.find_element_by_issue_number(issue_number)?, + )) + } + + pub fn iter_issues<'a>(&'a self) -> impl Iterator + 'a { + self.issue_map.keys().copied() + } + + pub fn subtree_for_element<'a>(&'a self, element: ElementIndex) -> Subtree<'a> { + Subtree::new_for_element(self, element) + } + + pub fn to_dot(&self) -> String { + let dot = petgraph::dot::Dot::with_attr_getters( + &self.graph, + &[ + petgraph::dot::Config::EdgeNoLabel, + petgraph::dot::Config::NodeNoLabel, + petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT), + ], + &|_g, _edge_id| "".to_string(), + &|_g, (_, element)| element.to_dot_node_attributes(None), + ); + + format!("{:?}", dot) + } + + pub fn to_mermaid(&self) -> String { + let mut mermaid = String::new(); + mermaid.push_str("flowchart BT\n"); + mermaid.push_str(&mermaid_classes()); + + for index in self.graph.node_indices() { + mermaid.push_str(&self.graph[index].to_mermaid_node(index, None)); + mermaid.push_str("\n"); + } + + for edge in self.graph.edge_references() { + mermaid.push_str(&format!( + " {} --> {}\n", + edge.source().index(), + edge.target().index() + )); + } + + mermaid + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] +pub struct SubtreeElement<'a> { + element: &'a Element, + role: SubtreeElementRole, +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)] +pub enum SubtreeElementRole { + ElementOfInterest, + Dependency, + Dependant, +} + +pub struct Subtree<'a> { + original: &'a Tree, + graph: petgraph::Graph, ()>, + index_map: BTreeMap, +} + +impl<'a> Subtree<'a> { + pub fn new_for_element(tree: &'a Tree, element: ElementIndex) -> Subtree<'a> { + let mut this = Self { + original: tree, + graph: petgraph::Graph::new(), + index_map: BTreeMap::new(), + }; + let graph = &this.original.graph; + + this.add_node_if_missing(element, SubtreeElementRole::ElementOfInterest); + + let mut dfs = petgraph::visit::Dfs::new(&graph, element); + while let Some(idx) = dfs.next(&graph) { + this.add_node_if_missing(idx, SubtreeElementRole::Dependency); + } + + for idx in this.index_map.keys().copied() { + for neighbor in graph.neighbors_directed(idx, petgraph::Direction::Outgoing) { + if !this.index_map.contains_key(&neighbor) { + log::warn!( + "Probably missed a dependency from #{from} to #{to} while generating subtree for #{el}", + el = graph[element].issue_number, + from = graph[idx].issue_number, + to = graph[neighbor].issue_number + ); + continue; + } + + this.graph + .add_edge(this.index_map[&idx], this.index_map[&neighbor], ()); + } + } + + for neighbor in graph.neighbors_directed(element, petgraph::Direction::Incoming) { + if this.index_map.contains_key(&neighbor) { + log::warn!( + "Already found #{from} in dependencies of #{to}, but it should have been the other way around?!", + from = graph[neighbor].issue_number, + to = graph[element].issue_number + ); + } else { + this.add_node_if_missing(neighbor, SubtreeElementRole::Dependant); + } + + this.graph + .add_edge(this.index_map[&neighbor], this.index_map[&element], ()); + } + + this + } + + fn add_node_if_missing(&mut self, index: ElementIndex, role: SubtreeElementRole) -> bool { + if self.index_map.contains_key(&index) { + debug_assert!(self.graph.node_weight(self.index_map[&index]).is_some()); + return false; + } + + let subtree_element = SubtreeElement { + element: &self.original.graph[index], + role, + }; + + let new_idx = self.graph.add_node(subtree_element); + self.index_map.insert(index, new_idx); + + return true; + } + + pub fn to_dot(&self) -> String { + let dot = petgraph::dot::Dot::with_attr_getters( + &self.graph, + &[ + petgraph::dot::Config::EdgeNoLabel, + petgraph::dot::Config::NodeNoLabel, + petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT), + ], + &|_g, _edge_id| "".to_string(), + &|_g, (_, element)| element.element.to_dot_node_attributes(Some(element.role)), + ); + + format!("{:?}", dot) + } + + pub fn to_mermaid(&self) -> String { + let mut mermaid = String::new(); + mermaid.push_str("flowchart BT\n"); + mermaid.push_str(&mermaid_classes()); + + for index in self.graph.node_indices() { + let node = &self.graph[index]; + mermaid.push_str(&node.element.to_mermaid_node(index, Some(node.role))); + mermaid.push_str("\n"); + } + + for edge in self.graph.edge_references() { + mermaid.push_str(&format!( + " {} --> {}\n", + edge.source().index(), + edge.target().index() + )); + } + + mermaid + } + + pub fn stable_hash(&self) -> String { + let mut nodes: Vec<_> = self.graph.node_weights().collect(); + nodes.sort_by_key(|n| n.element.issue_number); + let mut edges: Vec<_> = self + .graph + .edge_references() + .map(|edge| { + ( + self.graph[edge.source()].element.issue_number, + self.graph[edge.target()].element.issue_number, + ) + }) + .collect(); + edges.sort(); + + let json_data = serde_json::ser::to_string(&(nodes, edges)).unwrap(); + sha256::digest(json_data) + } +} diff --git a/techtree-manager/src/wiki.rs b/techtree-manager/src/wiki.rs new file mode 100644 index 0000000..65c0bbe --- /dev/null +++ b/techtree-manager/src/wiki.rs @@ -0,0 +1,24 @@ +use anyhow::Context as _; + +pub async fn update_wiki_overview( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::RepoMeta, + timestamp: String, + new_body: String, +) -> anyhow::Result<()> { + // TODO: Figure out why we get a 404 when the edit was successfull... + let _ = forgejo + .repo_edit_wiki_page( + &meta.owner, + &meta.name, + "Home", + forgejo_api::structs::CreateWikiPageOptions { + content_base64: Some(base64::encode(new_body.as_bytes())), + message: Some(format!("Updated to latest model at {timestamp}")), + title: Some("Home".to_owned()), + }, + ) + .await + .context("Failed editing the wiki page"); + Ok(()) +} From 04047052e32b64bb1f77f40b1c4b1df173e3f8cc Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 09:49:08 +0200 Subject: [PATCH 03/63] manager: Add nix shell This nix shell will also be used as the CI environment for this tool. --- techtree-manager/shell.nix | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 techtree-manager/shell.nix diff --git a/techtree-manager/shell.nix b/techtree-manager/shell.nix new file mode 100644 index 0000000..c862d91 --- /dev/null +++ b/techtree-manager/shell.nix @@ -0,0 +1,19 @@ +# From 2025-05-21 +let pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/36ecfe6216f0aa7f2a1ffe5aafc2c0eae6c8cdcf.tar.gz")) {}; + +in pkgs.mkShell { + buildInputs = [ + # Rust + pkgs.cargo + pkgs.rustc + pkgs.rustfmt + + # Dependencies + pkgs.openssl + ]; + + shellHook = '' + export OPENSSL_DIR="${pkgs.openssl.dev}" + export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib" + ''; +} From 77abf68e3c8b09434ffcf1b02499497d74c9029b Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 09:49:33 +0200 Subject: [PATCH 04/63] ci: Add CI workflows The push.yml workflow builds the manager and then places it in the $HOME of the runner. The issue.ym then uses the cached tool binary to update the techtree information whenever any issue is changed. This approach of course only works as long as only one runner is connected to this repository. In the long run, this approach shall be replaced by a nix build of the techtree-manager which is then cached by the OS instead of manually. --- .forgejo/workflows/issue.yml | 14 ++++++++++++++ .forgejo/workflows/push.yml | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .forgejo/workflows/issue.yml create mode 100644 .forgejo/workflows/push.yml diff --git a/.forgejo/workflows/issue.yml b/.forgejo/workflows/issue.yml new file mode 100644 index 0000000..b58a047 --- /dev/null +++ b/.forgejo/workflows/issue.yml @@ -0,0 +1,14 @@ +on: + issues: + types: [opened, reopened, closed, labeled, unlabeled, edited] + +jobs: + test: + runs-on: self-hosted-nixos-x86_64 + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + - name: Check techtree-manager presence + run: | + test -e ~/.cache/fafo-techtree/techtree-manager + - name: Run techtree-manager + run: cd techtree-manager/ && nix-shell shell.nix --run ~/.cache/fafo-techtree/techtree-manager diff --git a/.forgejo/workflows/push.yml b/.forgejo/workflows/push.yml new file mode 100644 index 0000000..733d119 --- /dev/null +++ b/.forgejo/workflows/push.yml @@ -0,0 +1,14 @@ +on: + push: + +jobs: + build: + runs-on: self-hosted-nixos-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + - name: Build techtree manager tool + run: cd techtree-manager/ && nix-shell shell.nix --run "cargo build --release" + - name: Cache the techtree manager tool + run: | + mkdir -p ~/.cache/fafo-techtree + cp -v techtree-manager/target/release/techtree-manager ~/.cache/fafo-techtree/techtree-manager From fb3a4a0d19015b0881563e16d00a0f8166fc7e7e Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 09:45:33 +0200 Subject: [PATCH 05/63] tree: Improve dot styling Use the same/similar styles that we use for mermaid. --- techtree-manager/src/tree.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 44b8be0..e65410b 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -27,19 +27,21 @@ impl Element { let mut attributes = Vec::new(); attributes.push(format!( - r##"label = "{{{{#{issue_number} | {status}}}|{ty}|{description}}}""## + r##"label = <{{{{#{issue_number} | {status}}}|{ty}|{description}}}>"## )); attributes.push(r#"shape = "record""#.to_owned()); - let color = match (role, status) { - (Some(SubtreeElementRole::ElementOfInterest), _) => "black", - (Some(SubtreeElementRole::Dependant), _) => "gray", - (_, ElementStatus::Missing) => "darkred", - (_, ElementStatus::Assigned) => "orange", - (_, ElementStatus::Completed) => "darkgreen", + let (color, background) = match (role, status) { + (Some(SubtreeElementRole::ElementOfInterest), _) => ("black", "white"), + (Some(SubtreeElementRole::Dependant), _) => ("gray", "gray"), + (_, ElementStatus::Missing) => ("#800", "#fcc"), + (_, ElementStatus::Assigned) => ("#a50", "#ffa"), + (_, ElementStatus::Completed) => ("#080", "#afa"), }; attributes.push(format!(r#"color = "{color}""#)); attributes.push(format!(r#"fontcolor = "{color}""#)); + attributes.push(format!(r#"fillcolor = "{background}""#)); + attributes.push(format!(r#"style = "filled""#)); attributes.join(", ") } From 69680a596b530e2024c2eaeead82cf4b2d14f2ae Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 09:55:43 +0200 Subject: [PATCH 06/63] Update README --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9d60249..d95b169 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -GUFTT -===== - -Grand Unified FAFO Tech Tree - -Work in progress during FOF1. +FAFO Technology Tree +==================== +This repository tracks our "technology tree" — the steps towards our +overarching goals. You can find a full representation of the tree in the +[Project Wiki](https://git.fa-fo.de/rahix/techtree-poc/wiki). From 81932d2ac1176be92c64f979e102bad542d31e91 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 10:09:22 +0200 Subject: [PATCH 07/63] manager: collect: Fix panic on non-tracked dependency --- techtree-manager/src/collect.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 4b06f6e..c580db4 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -1,5 +1,4 @@ use anyhow::Context as _; -use std::collections::BTreeMap; /// Read all issues to generate the full techtree pub async fn collect_tree( @@ -66,9 +65,9 @@ pub async fn collect_tree( let dep_number = dep.number.context("Missing issue number in dependency")?; if !tree.find_element_by_issue_number(dep_number).is_some() { log::warn!("Found dependency from #{issue} on non-tracked issue #{dep_number}!"); + } else { + tree.add_dependency_by_issue_number(issue, dep_number); } - - tree.add_dependency_by_issue_number(issue, dep_number); } } From 6d258f489fac5c1fa5ffc98431b15b697ba90fd7 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 10:09:59 +0200 Subject: [PATCH 08/63] manager: Use Type/ labels instead of ty/ and don't force types Allow arbitrary element types via the Type/# labels. --- techtree-manager/src/collect.rs | 14 +++++++------- techtree-manager/src/tree.rs | 23 ++++------------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index c580db4..3c03497 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -88,7 +88,7 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result anyhow::Result crate::tree::ElementType::Equipment, - "ty/process" => crate::tree::ElementType::Process, - "ty/knowledge" => crate::tree::ElementType::Knowledge, - t => anyhow::bail!("Unknown element type for issue #{issue_number}: {t:?}"), - }; + let ty = ty_labels + .first() + .unwrap() + .strip_prefix("Type/") + .unwrap() + .to_owned(); let status = match issue.state.context("Missing issue state")? { forgejo_api::structs::StateType::Open => { diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index e65410b..58e8fe1 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -10,7 +10,7 @@ pub struct Element { /// Description of this element pub description: String, /// Type of this element - pub ty: ElementType, + pub ty: String, /// Completion status of this element. pub status: ElementStatus, } @@ -54,7 +54,9 @@ impl Element { status, } = self; - let label = format!(r##"#{issue_number} | {status}\n{ty}\n{description}"##); + let label = format!( + r##"#{issue_number} | {status}\n{ty}\n{description}"## + ); let class = match (role, status) { (Some(SubtreeElementRole::ElementOfInterest), _) => "eoi", @@ -82,23 +84,6 @@ fn mermaid_classes() -> String { .to_owned() } -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)] -pub enum ElementType { - Equipment, - Process, - Knowledge, -} - -impl std::fmt::Display for ElementType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ElementType::Equipment => "Equipment", - ElementType::Process => "Process", - ElementType::Knowledge => "Knowledge", - }) - } -} - #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize)] pub enum ElementStatus { Missing, From 3bd79f176e5d49c434f15292fe07ce535b1e4b0e Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 10:23:07 +0200 Subject: [PATCH 09/63] manager: Apply rustfmt --- techtree-manager/src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index d5933ce..6dfe71f 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -66,9 +66,14 @@ async fn run() -> anyhow::Result<()> { "## ); log::info!("Updating the wiki overview..."); - wiki::update_wiki_overview(&forgejo, &meta.issue.repository, timestamp.to_string(), wiki_text) - .await - .context("Failed to update the techtree wiki page")?; + wiki::update_wiki_overview( + &forgejo, + &meta.issue.repository, + timestamp.to_string(), + wiki_text, + ) + .await + .context("Failed to update the techtree wiki page")?; 'issues: for issue in tree.iter_issues() { let subtree = tree.subtree_for_issue(issue).unwrap(); From 8b66d2551a997dad2aa0b269744bbdd001f27fa0 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 14:44:25 +0200 Subject: [PATCH 10/63] manager: Render graphviz tree and push it to git --- techtree-manager/.gitignore | 1 + techtree-manager/src/main.rs | 12 +++++- techtree-manager/src/render.rs | 69 ++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 techtree-manager/src/render.rs diff --git a/techtree-manager/.gitignore b/techtree-manager/.gitignore index b83d222..00df608 100644 --- a/techtree-manager/.gitignore +++ b/techtree-manager/.gitignore @@ -1 +1,2 @@ /target/ +/render-git/ diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 6dfe71f..cef79ca 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -4,6 +4,7 @@ use forgejo_api::Forgejo; mod collect; mod event_meta; mod issue; +mod render; mod tree; mod wiki; @@ -21,7 +22,7 @@ async fn run() -> anyhow::Result<()> { meta.issue.number ); - let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); log::info!("Timestamp of this run is {timestamp}"); let token = @@ -33,6 +34,10 @@ async fn run() -> anyhow::Result<()> { ) .context("Failed parsing GITHUB_SERVER_URL as a url")?; + let mut repo_auth_url = server_url.join(&format!("{}/{}", meta.issue.repository.owner, meta.issue.repository.name)).unwrap(); + repo_auth_url.set_username("forgejo-actions").unwrap(); + repo_auth_url.set_password(Some(&token)).unwrap(); + let forgejo = Forgejo::new(auth, server_url).context("Could not create API access object")?; let new_comment_id = if meta.action == event_meta::IssueAction::Opened { @@ -56,6 +61,11 @@ async fn run() -> anyhow::Result<()> { .await .context("Failed to collect the techtree from issue metadata")?; + log::info!("Rendering and publishing techtree to git repository..."); + render::render_and_publish(&tree, ×tamp, &repo_auth_url) + .await + .context("Failed to render and publish the techtree to git")?; + let mermaid = tree.to_mermaid(); let wiki_text = format!( r##"This page is automatically updated to show the latest and greatest FAFO techtree: diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs new file mode 100644 index 0000000..9373d2d --- /dev/null +++ b/techtree-manager/src/render.rs @@ -0,0 +1,69 @@ +use std::process::Command; + +use anyhow::Context as _; + +pub async fn render_and_publish( + tree: &crate::tree::Tree, + timestamp: &str, + repo_auth_url: &url::Url, +) -> anyhow::Result<()> { + let render_repo = std::path::PathBuf::from("render-git"); + + if render_repo.is_dir() { + log::info!("Found old {render_repo:?} repository, removing..."); + std::fs::remove_dir_all(&render_repo).context("Failed to remove stale render repository")?; + } + + std::fs::create_dir(&render_repo).context("Failed creating directory for rendered graph")?; + + Command::new("git") + .arg("-C") + .arg(&render_repo) + .arg("init") + .status() + .context("Failed to initialize render repository")?; + + let dot_file = render_repo.join("techtree.dot"); + let svg_file = render_repo.join("techtree.svg"); + + let dot_source = tree.to_dot(); + std::fs::write(&dot_file, dot_source.as_bytes()) + .context("Failed to write `dot` source file")?; + + Command::new("dot") + .args(["-T", "svg"]) + .arg("-o") + .arg(&svg_file) + .arg(&dot_file) + .status() + .context("Failed to generate svg graph from dot source")?; + + Command::new("git") + .arg("-C") + .arg(&render_repo) + .arg("add") + .arg(dot_file.file_name().unwrap()) + .arg(svg_file.file_name().unwrap()) + .status() + .context("Failed to add generated graph files to git index")?; + + Command::new("git") + .arg("-C") + .arg(&render_repo) + .arg("commit") + .args(["-m", &format!("Updated techtree at {timestamp}")]) + .status() + .context("Failed to add generated graph files to git index")?; + + Command::new("git") + .arg("-C") + .arg(&render_repo) + .arg("push") + .arg("--force") + .arg(repo_auth_url.to_string()) + .arg("HEAD:refs/heads/render") + .status() + .context("Failed to push rendered graph to forgejo repository")?; + + Ok(()) +} From 656b8f4b2514f14b2b50a109b782079209e8f390 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 14:44:44 +0200 Subject: [PATCH 11/63] manager: Add new dependencies to nix shell --- techtree-manager/shell.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/techtree-manager/shell.nix b/techtree-manager/shell.nix index c862d91..c893b75 100644 --- a/techtree-manager/shell.nix +++ b/techtree-manager/shell.nix @@ -10,6 +10,8 @@ in pkgs.mkShell { # Dependencies pkgs.openssl + pkgs.graphviz + pkgs.git ]; shellHook = '' From 9198ffa10a3159167605cfe740970c7527831205 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 14:45:40 +0200 Subject: [PATCH 12/63] README: Reference rendered tree --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d95b169..3ad60fd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ FAFO Technology Tree ==================== This repository tracks our "technology tree" — the steps towards our -overarching goals. You can find a full representation of the tree in the -[Project Wiki](https://git.fa-fo.de/rahix/techtree-poc/wiki). +overarching goals. + +## Full Technology Tree +Below, you can see the full technology tree. The issues in this repository +track the elements it contains. For better overview, check each issue for its +relevant subtree. + +![FAFO Tech Tree](https://git.fa-fo.de/rahix/techtree-poc/raw/branch/render/techtree.svg) From cf484fe21ccf1027ef7e817ca0bdc0adadf02366 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:00:28 +0200 Subject: [PATCH 13/63] manager: Fix ignoring subcommand exit status --- techtree-manager/src/render.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 9373d2d..4fcb184 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -2,6 +2,19 @@ use std::process::Command; use anyhow::Context as _; +trait SuccessExt { + fn success(&mut self) -> anyhow::Result<()>; +} + +impl SuccessExt for Command { + fn success(&mut self) -> anyhow::Result<()> { + if !self.status()?.success() { + anyhow::bail!("Command returned unsuccessfully"); + } + Ok(()) + } +} + pub async fn render_and_publish( tree: &crate::tree::Tree, timestamp: &str, @@ -20,7 +33,7 @@ pub async fn render_and_publish( .arg("-C") .arg(&render_repo) .arg("init") - .status() + .success() .context("Failed to initialize render repository")?; let dot_file = render_repo.join("techtree.dot"); @@ -35,7 +48,7 @@ pub async fn render_and_publish( .arg("-o") .arg(&svg_file) .arg(&dot_file) - .status() + .success() .context("Failed to generate svg graph from dot source")?; Command::new("git") @@ -44,7 +57,7 @@ pub async fn render_and_publish( .arg("add") .arg(dot_file.file_name().unwrap()) .arg(svg_file.file_name().unwrap()) - .status() + .success() .context("Failed to add generated graph files to git index")?; Command::new("git") @@ -52,7 +65,7 @@ pub async fn render_and_publish( .arg(&render_repo) .arg("commit") .args(["-m", &format!("Updated techtree at {timestamp}")]) - .status() + .success() .context("Failed to add generated graph files to git index")?; Command::new("git") From e274f6206cc95b986e5bf9d112a79b0307b31ffa Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:02:57 +0200 Subject: [PATCH 14/63] manager: Configure a git identity for committing --- techtree-manager/src/render.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 4fcb184..3cc9d5f 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -36,6 +36,20 @@ pub async fn render_and_publish( .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"); From f04beb5dea5e2c2a6746f406f3cb184728d68c1a Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:24:57 +0200 Subject: [PATCH 15/63] manager: Remove `Stale` label if present The stale label provides an easy way to force CI runs for issues after changing dependencies. --- techtree-manager/Cargo.lock | 1 + techtree-manager/Cargo.toml | 1 + techtree-manager/src/issue.rs | 37 +++++++++++++++++++++++++++++++++++ techtree-manager/src/main.rs | 10 ++++++++++ 4 files changed, 49 insertions(+) diff --git a/techtree-manager/Cargo.lock b/techtree-manager/Cargo.lock index 614a664..05548ee 100644 --- a/techtree-manager/Cargo.lock +++ b/techtree-manager/Cargo.lock @@ -1349,6 +1349,7 @@ dependencies = [ "serde", "serde_json", "sha256", + "time", "tokio", "url", ] diff --git a/techtree-manager/Cargo.toml b/techtree-manager/Cargo.toml index afbfdcd..a2e6f82 100644 --- a/techtree-manager/Cargo.toml +++ b/techtree-manager/Cargo.toml @@ -14,5 +14,6 @@ petgraph = "0.8.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" sha256 = "1.6.0" +time = "0.3.41" tokio = { version = "1.45.0", features = ["full"] } url = "2.5.4" diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index 7f1498c..2a74293 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -78,5 +78,42 @@ pub async fn update_bot_comment( ) .await .context("Failed to update comment body")?; + + Ok(()) +} + +pub async fn remove_stale_label( + forgejo: &forgejo_api::Forgejo, + meta: &crate::event_meta::RepoMeta, + issue_number: u64, +) -> anyhow::Result<()> { + let labels = forgejo + .issue_get_labels(&meta.owner, &meta.name, issue_number) + .await + .context("Failed fetching issue labels")?; + + let stale_label_id = labels + .iter() + .filter(|l| l.name.as_deref() == Some("Stale")) + .next() + .map(|l| l.id.unwrap()); + + if let Some(stale_label_id) = stale_label_id { + log::info!("Removing `Stale` label from issue #{issue_number}..."); + + forgejo + .issue_remove_label( + &meta.owner, + &meta.name, + issue_number, + stale_label_id, + forgejo_api::structs::DeleteLabelsOption { + updated_at: Some(time::OffsetDateTime::now_utc()), + }, + ) + .await + .context("Failed to remove the `Stale` label")?; + } + Ok(()) } diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index cef79ca..2e487dc 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -99,6 +99,12 @@ async fn run() -> anyhow::Result<()> { if let Some(bot_comment) = bot_comment { if bot_comment.body.contains(&hash) { log::info!("Issue #{issue} is up-to-date, not editing comment."); + issue::remove_stale_label(&forgejo, &meta.issue.repository, issue) + .await + .with_context(|| { + format!("Failed to remove `Stale` label from issue #{issue}") + })?; + continue 'issues; } bot_comment.id @@ -128,6 +134,10 @@ async fn run() -> anyhow::Result<()> { issue::update_bot_comment(&forgejo, &meta.issue.repository, comment_id, full_text) .await .with_context(|| format!("Failed to update the bot comment in issue #{issue}"))?; + + issue::remove_stale_label(&forgejo, &meta.issue.repository, issue) + .await + .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?; } Ok(()) From d0c8389c3ba195c0f91c7610bada0f16f95c22be Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:25:19 +0200 Subject: [PATCH 16/63] manager: Apply rustfmt --- techtree-manager/src/main.rs | 7 ++++++- techtree-manager/src/render.rs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 2e487dc..ed5678f 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -34,7 +34,12 @@ async fn run() -> anyhow::Result<()> { ) .context("Failed parsing GITHUB_SERVER_URL as a url")?; - let mut repo_auth_url = server_url.join(&format!("{}/{}", meta.issue.repository.owner, meta.issue.repository.name)).unwrap(); + let mut repo_auth_url = server_url + .join(&format!( + "{}/{}", + meta.issue.repository.owner, meta.issue.repository.name + )) + .unwrap(); repo_auth_url.set_username("forgejo-actions").unwrap(); repo_auth_url.set_password(Some(&token)).unwrap(); diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 3cc9d5f..6d77f0d 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -24,7 +24,8 @@ pub async fn render_and_publish( if render_repo.is_dir() { log::info!("Found old {render_repo:?} repository, removing..."); - std::fs::remove_dir_all(&render_repo).context("Failed to remove stale render repository")?; + std::fs::remove_dir_all(&render_repo) + .context("Failed to remove stale render repository")?; } std::fs::create_dir(&render_repo).context("Failed creating directory for rendered graph")?; From 24e834dde3aed34af3c6364146ed8a0b5b11a49c Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:40:18 +0200 Subject: [PATCH 17/63] manager: Don't hardcode repository URL --- techtree-manager/src/main.rs | 7 ++++--- techtree-manager/src/tree.rs | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index ed5678f..6be028c 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -34,12 +34,13 @@ async fn run() -> anyhow::Result<()> { ) .context("Failed parsing GITHUB_SERVER_URL as a url")?; - let mut repo_auth_url = server_url + let repo_url = server_url .join(&format!( "{}/{}", 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(); @@ -71,7 +72,7 @@ async fn run() -> anyhow::Result<()> { .await .context("Failed to render and publish the techtree to git")?; - let mermaid = tree.to_mermaid(); + let mermaid = tree.to_mermaid(&repo_url.to_string()); let wiki_text = format!( r##"This page is automatically updated to show the latest and greatest FAFO techtree: @@ -124,7 +125,7 @@ async fn run() -> anyhow::Result<()> { } }; - let mermaid = subtree.to_mermaid(); + let mermaid = subtree.to_mermaid(&repo_url.to_string()); let full_text = format!( r##"## Partial Techtree diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 58e8fe1..6bb5217 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -46,7 +46,12 @@ impl Element { attributes.join(", ") } - fn to_mermaid_node(&self, index: ElementIndex, role: Option) -> String { + fn to_mermaid_node( + &self, + index: ElementIndex, + role: Option, + repo_url: &str, + ) -> String { let Element { issue_number, description, @@ -55,7 +60,7 @@ impl Element { } = self; let label = format!( - r##"#{issue_number} | {status}\n{ty}\n{description}"## + r##"#{issue_number} | {status}\n{ty}\n{description}"## ); let class = match (role, status) { @@ -163,13 +168,13 @@ impl Tree { format!("{:?}", dot) } - pub fn to_mermaid(&self) -> String { + pub fn to_mermaid(&self, repo_url: &str) -> String { let mut mermaid = String::new(); mermaid.push_str("flowchart BT\n"); mermaid.push_str(&mermaid_classes()); for index in self.graph.node_indices() { - mermaid.push_str(&self.graph[index].to_mermaid_node(index, None)); + mermaid.push_str(&self.graph[index].to_mermaid_node(index, None, repo_url)); mermaid.push_str("\n"); } @@ -287,14 +292,18 @@ impl<'a> Subtree<'a> { format!("{:?}", dot) } - pub fn to_mermaid(&self) -> String { + pub fn to_mermaid(&self, repo_url: &str) -> String { let mut mermaid = String::new(); mermaid.push_str("flowchart BT\n"); mermaid.push_str(&mermaid_classes()); for index in self.graph.node_indices() { let node = &self.graph[index]; - mermaid.push_str(&node.element.to_mermaid_node(index, Some(node.role))); + mermaid.push_str( + &node + .element + .to_mermaid_node(index, Some(node.role), repo_url), + ); mermaid.push_str("\n"); } From 6d7af36726e0e34de472623183ec5e27ba5b34ef Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 15:58:13 +0200 Subject: [PATCH 18/63] manager: Ignore errors while removing labels --- techtree-manager/src/issue.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index 2a74293..9f1f5b7 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -101,7 +101,7 @@ pub async fn remove_stale_label( if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); - forgejo + let res = forgejo .issue_remove_label( &meta.owner, &meta.name, @@ -111,8 +111,15 @@ pub async fn remove_stale_label( updated_at: Some(time::OffsetDateTime::now_utc()), }, ) - .await - .context("Failed to remove the `Stale` label")?; + .await; + + if let Err(e) = res { + // At the moment, the token for Forgejo Actions cannot remove issue labels. + // See https://codeberg.org/forgejo/forgejo/issues/2415 + log::warn!( + "Failed to remove `Stale` label for #{issue_number}. This is a known Forgejo limitation at the moment... ({e})" + ); + } } Ok(()) From c93f701ef88a31295f2881de0d7f614f872f00fa Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 16:04:55 +0200 Subject: [PATCH 19/63] manager: Allow dead_code For now, this makes development easier --- techtree-manager/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 6be028c..d89d2e0 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use anyhow::Context as _; use forgejo_api::Forgejo; From a6aa02aa40c9e67a62df1540075101d317d000a8 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 16:03:38 +0200 Subject: [PATCH 20/63] manager: wiki: Fix deprecation warning about base64::encode() --- techtree-manager/src/wiki.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/techtree-manager/src/wiki.rs b/techtree-manager/src/wiki.rs index 65c0bbe..647aa17 100644 --- a/techtree-manager/src/wiki.rs +++ b/techtree-manager/src/wiki.rs @@ -1,4 +1,5 @@ use anyhow::Context as _; +use base64::prelude::*; pub async fn update_wiki_overview( forgejo: &forgejo_api::Forgejo, @@ -13,7 +14,7 @@ pub async fn update_wiki_overview( &meta.name, "Home", forgejo_api::structs::CreateWikiPageOptions { - content_base64: Some(base64::encode(new_body.as_bytes())), + content_base64: Some(BASE64_STANDARD.encode(new_body.as_bytes())), message: Some(format!("Updated to latest model at {timestamp}")), title: Some("Home".to_owned()), }, From 34d7a371b3de732d5989c5d87f3bd1b458491920 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 16:02:10 +0200 Subject: [PATCH 21/63] manager: Drop wiki page updates Let's focus on the dot-based total tree --- techtree-manager/src/main.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index d89d2e0..379d94e 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -74,24 +74,26 @@ async fn run() -> anyhow::Result<()> { .await .context("Failed to render and publish the techtree to git")?; - let mermaid = tree.to_mermaid(&repo_url.to_string()); - let wiki_text = format!( - r##"This page is automatically updated to show the latest and greatest FAFO techtree: + if false { + let mermaid = tree.to_mermaid(&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..."); - wiki::update_wiki_overview( - &forgejo, - &meta.issue.repository, - timestamp.to_string(), - wiki_text, - ) - .await - .context("Failed to update the techtree wiki page")?; + ); + log::info!("Updating the wiki overview..."); + wiki::update_wiki_overview( + &forgejo, + &meta.issue.repository, + timestamp.to_string(), + wiki_text, + ) + .await + .context("Failed to update the techtree wiki page")?; + } 'issues: for issue in tree.iter_issues() { let subtree = tree.subtree_for_issue(issue).unwrap(); From ec5a71b936a71a2c6ef0d4abf58fce074b047754 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 16:59:40 +0200 Subject: [PATCH 22/63] manager: Make graph tree more tall than wide Helps with readability. --- techtree-manager/src/tree.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 6bb5217..bec1c64 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -160,12 +160,17 @@ impl Tree { petgraph::dot::Config::EdgeNoLabel, petgraph::dot::Config::NodeNoLabel, petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT), + petgraph::dot::Config::GraphContentOnly, ], &|_g, _edge_id| "".to_string(), &|_g, (_, element)| element.to_dot_node_attributes(None), ); - format!("{:?}", dot) + format!(r#"digraph {{ + ranksep=1.2 +{:?} +}} +"#, dot) } pub fn to_mermaid(&self, repo_url: &str) -> String { From 4d7bc0742e0d2aa54184c2a4a0a512db05e546e7 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 17:11:18 +0200 Subject: [PATCH 23/63] ci: Don't run manager on "issue unlabeled" --- .forgejo/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/issue.yml b/.forgejo/workflows/issue.yml index b58a047..57cda79 100644 --- a/.forgejo/workflows/issue.yml +++ b/.forgejo/workflows/issue.yml @@ -1,6 +1,6 @@ on: issues: - types: [opened, reopened, closed, labeled, unlabeled, edited] + types: [opened, reopened, closed, labeled, edited] jobs: test: From e418d9d720b01fe1128529704e46ba702a797f36 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 17:20:01 +0200 Subject: [PATCH 24/63] manager: Reduce noise from git commands --- techtree-manager/src/render.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 6d77f0d..40cefad 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -34,6 +34,8 @@ pub async fn render_and_publish( .arg("-C") .arg(&render_repo) .arg("init") + .arg("--initial-branch=render") + .arg("--quiet") .success() .context("Failed to initialize render repository")?; @@ -88,6 +90,7 @@ pub async fn render_and_publish( .arg(&render_repo) .arg("push") .arg("--force") + .arg("--quiet") .arg(repo_auth_url.to_string()) .arg("HEAD:refs/heads/render") .status() From 34e9a99d6169ee6dec042602ff55ec845397039a Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 17:23:46 +0200 Subject: [PATCH 25/63] manager: Fix pagination limit for issue list It turns out that `None` does not disable pagination, it just uses the default limit. Set an obscenely high limit to ensure we get the full list. --- techtree-manager/src/collect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 3c03497..81a0971 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -13,7 +13,7 @@ pub async fn collect_tree( // We also want the closed issues state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), // No pagination - limit: None, + limit: Some(10000), // Only issues r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues), ..Default::default() @@ -49,7 +49,7 @@ pub async fn collect_tree( &meta.issue.repository.name, &issue.to_string(), forgejo_api::structs::IssueListIssueDependenciesQuery { - limit: None, + limit: Some(10000), ..Default::default() }, ) From f11d8459bab0b09a6907131a836de4dc3d0b5355 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 17:37:45 +0200 Subject: [PATCH 26/63] manager: Wait a bit before collecting new issue --- techtree-manager/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 379d94e..81cb520 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -51,7 +51,7 @@ async fn run() -> anyhow::Result<()> { let new_comment_id = if meta.action == event_meta::IssueAction::Opened { let res = issue::make_bot_comment(&forgejo, &meta, meta.issue.number).await; - match res { + let id = match res { Ok(id) => Some(id), Err(e) => { log::warn!( @@ -60,7 +60,12 @@ async fn run() -> anyhow::Result<()> { ); None } - } + }; + + 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; + + id } else { None }; From eebbd9c453ff8ddd2269035a2256293cb0649cf0 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 17:56:28 +0200 Subject: [PATCH 27/63] manager: Fix double comment bug When the job for the new issue is scheduled later than another manager run, we create a second comment due to the previously unconditional comment creating. Always look for an existing comment first. --- techtree-manager/src/main.rs | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 81cb520..5b7866a 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -49,20 +49,36 @@ async fn run() -> anyhow::Result<()> { let forgejo = Forgejo::new(auth, server_url).context("Could not create API access object")?; let new_comment_id = if meta.action == event_meta::IssueAction::Opened { - let res = issue::make_bot_comment(&forgejo, &meta, meta.issue.number).await; + let bot_comment = + issue::find_bot_comment(&forgejo, &meta.issue.repository, meta.issue.number) + .await + .with_context(|| { + format!( + "Failed searching for bot comment for issue #{}", + meta.issue.number + ) + })?; - let id = match res { - Ok(id) => Some(id), - Err(e) => { - log::warn!( - "Error while creating the informational comment on issue #{}:\n{e:?}", - meta.issue.number - ); - None + let id = match bot_comment { + Some(comment) => Some(comment.id), + None => { + let res = issue::make_bot_comment(&forgejo, &meta, 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 + } + } } }; - log::info!("Waiting for a minute, as the issue is brand new. We will likely catch some more updates this way!"); + 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; id From bc54cd91fdcf8184bcf35127f8db87172ddda2dc Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 18:47:31 +0200 Subject: [PATCH 28/63] manager: Factor out a common context object Clean up the global data a bit by bundling it in a context object. --- techtree-manager/src/collect.rs | 27 +++++------- techtree-manager/src/issue.rs | 40 ++++++++---------- techtree-manager/src/main.rs | 75 +++++++++++++++++++++------------ techtree-manager/src/render.rs | 7 ++- techtree-manager/src/wiki.rs | 12 +++--- 5 files changed, 84 insertions(+), 77 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 81a0971..79b23af 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -1,14 +1,12 @@ use anyhow::Context as _; /// Read all issues to generate the full techtree -pub async fn collect_tree( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::IssueEventMeta, -) -> anyhow::Result { - let issues = forgejo +pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result { + let issues = ctx + .forgejo .issue_list_issues( - &meta.issue.repository.owner, - &meta.issue.repository.name, + &ctx.owner, + &ctx.repo, forgejo_api::structs::IssueListIssuesQuery { // We also want the closed issues state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), @@ -43,10 +41,12 @@ pub async fn collect_tree( } for issue in issue_numbers.into_iter() { - let dependencies = forgejo + let dependencies = ctx + .forgejo .issue_list_issue_dependencies( - &meta.issue.repository.owner, - &meta.issue.repository.name, + &ctx.owner, + &ctx.repo, + // Why the hell is the issue number a string here? &issue.to_string(), forgejo_api::structs::IssueListIssueDependenciesQuery { limit: Some(10000), @@ -54,12 +54,7 @@ pub async fn collect_tree( }, ) .await - .with_context(|| { - format!( - "Failed to fetch issue dependencies for #{}", - meta.issue.number - ) - })?; + .with_context(|| format!("Failed to fetch issue dependencies for #{issue}",))?; for dep in dependencies { let dep_number = dep.number.context("Missing issue number in dependency")?; diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index 9f1f5b7..d87b852 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -8,17 +8,16 @@ pub struct BotCommentInfo { } pub async fn make_bot_comment( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::IssueEventMeta, + ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result { let initial_message = "_Please be patient, this issue is currently being integrated into the techtree..._"; - let res = forgejo + let res = ctx.forgejo .issue_create_comment( - &meta.issue.repository.owner, - &meta.issue.repository.name, + &ctx.owner, + &ctx.repo, issue_number, forgejo_api::structs::CreateIssueCommentOption { body: initial_message.to_owned(), @@ -31,14 +30,13 @@ pub async fn make_bot_comment( } pub async fn find_bot_comment( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::RepoMeta, + ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result> { - let mut comments = forgejo + let mut comments = ctx.forgejo .issue_get_comments( - &meta.owner, - &meta.name, + &ctx.owner, + &ctx.repo, issue_number, forgejo_api::structs::IssueGetCommentsQuery { ..Default::default() @@ -61,15 +59,14 @@ pub async fn find_bot_comment( } pub async fn update_bot_comment( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::RepoMeta, + ctx: &crate::Context, id: CommentId, new_body: String, ) -> anyhow::Result<()> { - forgejo + ctx.forgejo .issue_edit_comment( - &meta.owner, - &meta.name, + &ctx.owner, + &ctx.repo, id, forgejo_api::structs::EditIssueCommentOption { body: new_body, @@ -83,12 +80,11 @@ pub async fn update_bot_comment( } pub async fn remove_stale_label( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::RepoMeta, + ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result<()> { - let labels = forgejo - .issue_get_labels(&meta.owner, &meta.name, issue_number) + let labels = ctx.forgejo + .issue_get_labels(&ctx.owner, &ctx.repo, issue_number) .await .context("Failed fetching issue labels")?; @@ -101,10 +97,10 @@ pub async fn remove_stale_label( if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); - let res = forgejo + let res = ctx.forgejo .issue_remove_label( - &meta.owner, - &meta.name, + &ctx.owner, + &ctx.repo, issue_number, stale_label_id, forgejo_api::structs::DeleteLabelsOption { diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 5b7866a..4902e7a 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -10,6 +10,21 @@ 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: url::Url, + /// 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!"); @@ -48,21 +63,29 @@ async fn run() -> anyhow::Result<()> { 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 bot_comment = - issue::find_bot_comment(&forgejo, &meta.issue.repository, meta.issue.number) - .await - .with_context(|| { - format!( - "Failed searching for bot comment for issue #{}", - meta.issue.number - ) - })?; + let bot_comment = issue::find_bot_comment(&ctx, meta.issue.number) + .await + .with_context(|| { + format!( + "Failed searching for bot comment for issue #{}", + meta.issue.number + ) + })?; let id = match bot_comment { Some(comment) => Some(comment.id), 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 { Ok(id) => Some(id), Err(e) => { @@ -86,17 +109,17 @@ async fn run() -> anyhow::Result<()> { None }; - let tree = collect::collect_tree(&forgejo, &meta) + 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..."); - render::render_and_publish(&tree, ×tamp, &repo_auth_url) + render::render_and_publish(&ctx, &tree) .await .context("Failed to render and publish the techtree to git")?; 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!( r##"This page is automatically updated to show the latest and greatest FAFO techtree: @@ -106,14 +129,9 @@ async fn run() -> anyhow::Result<()> { "## ); log::info!("Updating the wiki overview..."); - wiki::update_wiki_overview( - &forgejo, - &meta.issue.repository, - timestamp.to_string(), - wiki_text, - ) - .await - .context("Failed to update the techtree wiki page")?; + wiki::update_wiki_overview(&ctx, wiki_text) + .await + .context("Failed to update the techtree wiki page")?; } 'issues: for issue in tree.iter_issues() { @@ -123,14 +141,14 @@ async fn run() -> anyhow::Result<()> { let comment_id = if new_comment_id.is_some() && issue == meta.issue.number { new_comment_id.unwrap() } else { - let bot_comment = issue::find_bot_comment(&forgejo, &meta.issue.repository, issue) + let bot_comment = issue::find_bot_comment(&ctx, issue) .await .with_context(|| format!("Failed searching for bot comment for issue #{issue}"))?; if let Some(bot_comment) = bot_comment { if bot_comment.body.contains(&hash) { 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 .with_context(|| { format!("Failed to remove `Stale` label from issue #{issue}") @@ -142,7 +160,7 @@ async fn run() -> anyhow::Result<()> { } else { log::warn!("Missing bot comment in issue #{issue}"); - issue::make_bot_comment(&forgejo, &meta, issue) + issue::make_bot_comment(&ctx, issue) .await .with_context(|| { 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!( r##"## Partial Techtree @@ -158,15 +176,16 @@ async fn run() -> anyhow::Result<()> { {mermaid} ``` -Digest: {hash}; Last Updated: {timestamp}"## +Digest: {hash}; Last Updated: {timestamp}"##, + timestamp = ctx.timestamp, ); 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 .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 .with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?; } diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 40cefad..69682e9 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -16,9 +16,8 @@ impl SuccessExt for Command { } pub async fn render_and_publish( + ctx: &crate::Context, tree: &crate::tree::Tree, - timestamp: &str, - repo_auth_url: &url::Url, ) -> anyhow::Result<()> { let render_repo = std::path::PathBuf::from("render-git"); @@ -81,7 +80,7 @@ pub async fn render_and_publish( .arg("-C") .arg(&render_repo) .arg("commit") - .args(["-m", &format!("Updated techtree at {timestamp}")]) + .args(["-m", &format!("Updated techtree at {}", ctx.timestamp)]) .success() .context("Failed to add generated graph files to git index")?; @@ -91,7 +90,7 @@ pub async fn render_and_publish( .arg("push") .arg("--force") .arg("--quiet") - .arg(repo_auth_url.to_string()) + .arg(ctx.repo_auth_url.to_string()) .arg("HEAD:refs/heads/render") .status() .context("Failed to push rendered graph to forgejo repository")?; diff --git a/techtree-manager/src/wiki.rs b/techtree-manager/src/wiki.rs index 647aa17..0f1de44 100644 --- a/techtree-manager/src/wiki.rs +++ b/techtree-manager/src/wiki.rs @@ -2,20 +2,18 @@ use anyhow::Context as _; use base64::prelude::*; pub async fn update_wiki_overview( - forgejo: &forgejo_api::Forgejo, - meta: &crate::event_meta::RepoMeta, - timestamp: String, + ctx: &crate::Context, new_body: String, ) -> anyhow::Result<()> { // TODO: Figure out why we get a 404 when the edit was successfull... - let _ = forgejo + let _ = ctx.forgejo .repo_edit_wiki_page( - &meta.owner, - &meta.name, + &ctx.owner, + &ctx.repo, "Home", forgejo_api::structs::CreateWikiPageOptions { 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()), }, ) From 785ff3710769cf811075afab8fc016acff26e489 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 20:27:08 +0200 Subject: [PATCH 29/63] manager: The big refactoring Clean up the logic in main to make it more readable. Factor out common functionality into the respective modules. --- techtree-manager/src/issue.rs | 64 ++++++++++++++--- techtree-manager/src/main.rs | 124 ++++++++++----------------------- techtree-manager/src/render.rs | 93 ++++++++++++++----------- techtree-manager/src/tree.rs | 7 +- techtree-manager/src/wiki.rs | 23 +++++- 5 files changed, 171 insertions(+), 140 deletions(-) diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index d87b852..3fd50c7 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -10,11 +10,12 @@ pub struct BotCommentInfo { pub async fn make_bot_comment( ctx: &crate::Context, issue_number: u64, -) -> anyhow::Result { +) -> anyhow::Result { let initial_message = "_Please be patient, this issue is currently being integrated into the techtree..._"; - let res = ctx.forgejo + let res = ctx + .forgejo .issue_create_comment( &ctx.owner, &ctx.repo, @@ -26,14 +27,18 @@ pub async fn make_bot_comment( ) .await?; - Ok(res.id.unwrap()) + Ok(BotCommentInfo { + id: res.id.unwrap(), + body: initial_message.to_owned(), + }) } pub async fn find_bot_comment( ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result> { - let mut comments = ctx.forgejo + let mut comments = ctx + .forgejo .issue_get_comments( &ctx.owner, &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( ctx: &crate::Context, id: CommentId, @@ -79,11 +105,32 @@ pub async fn update_bot_comment( Ok(()) } -pub async fn remove_stale_label( +pub async fn update_bot_comment_from_subtree( ctx: &crate::Context, - issue_number: u64, + id: CommentId, + subtree: &crate::tree::Subtree<'_>, + hash: &str, ) -> 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} +``` + +Digest: {hash}; Last Updated: {timestamp}"##, + 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) .await .context("Failed fetching issue labels")?; @@ -97,7 +144,8 @@ pub async fn remove_stale_label( if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); - let res = ctx.forgejo + let res = ctx + .forgejo .issue_remove_label( &ctx.owner, &ctx.repo, diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 4902e7a..a8b6ef1 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -72,64 +72,43 @@ async fn run() -> anyhow::Result<()> { timestamp, }; - let new_comment_id = if meta.action == event_meta::IssueAction::Opened { - let bot_comment = issue::find_bot_comment(&ctx, meta.issue.number) - .await - .with_context(|| { - format!( - "Failed searching for bot comment for issue #{}", - meta.issue.number - ) - })?; - - 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 - } + 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; } } - }; - - 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; - - id - } else { - None - }; + 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..."); - render::render_and_publish(&ctx, &tree) + let rendered_tree = render::render(&ctx, &tree) .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 { - 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..."); - wiki::update_wiki_overview(&ctx, wiki_text) + wiki::update_wiki_from_tree(&ctx, &tree) .await .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 hash = subtree.stable_hash(); - let comment_id = if new_comment_id.is_some() && issue == meta.issue.number { - new_comment_id.unwrap() - } else { - let bot_comment = issue::find_bot_comment(&ctx, issue) + 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::info!("Issue #{issue} is up-to-date, not editing comment."); + issue::remove_stale_label(&ctx, issue) .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 { - 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} -``` - -Digest: {hash}; Last Updated: {timestamp}"##, - timestamp = ctx.timestamp, - ); + continue 'issues; + } 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 - .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) .await diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 69682e9..f3d75b9 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -15,45 +15,27 @@ impl SuccessExt for Command { } } -pub async fn render_and_publish( - ctx: &crate::Context, - tree: &crate::tree::Tree, -) -> anyhow::Result<()> { - let render_repo = std::path::PathBuf::from("render-git"); +pub struct RenderedTree { + repo_dir: std::path::PathBuf, + dot_file: std::path::PathBuf, + svg_file: std::path::PathBuf, +} - if render_repo.is_dir() { - log::info!("Found old {render_repo:?} repository, removing..."); - std::fs::remove_dir_all(&render_repo) - .context("Failed to remove stale render repository")?; +pub async fn render( + _ctx: &crate::Context, + tree: &crate::tree::Tree, +) -> anyhow::Result { + 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") - .arg("-C") - .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_file = repo_dir.join("techtree.dot"); + let svg_file = repo_dir.join("techtree.svg"); let dot_source = tree.to_dot(); std::fs::write(&dot_file, dot_source.as_bytes()) @@ -67,18 +49,49 @@ pub async fn render_and_publish( .success() .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") .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(dot_file.file_name().unwrap()) - .arg(svg_file.file_name().unwrap()) + .arg(rendered.dot_file.strip_prefix(&rendered.repo_dir).unwrap()) + .arg(rendered.svg_file.strip_prefix(&rendered.repo_dir).unwrap()) .success() .context("Failed to add generated graph files to git index")?; Command::new("git") .arg("-C") - .arg(&render_repo) + .arg(&rendered.repo_dir) .arg("commit") .args(["-m", &format!("Updated techtree at {}", ctx.timestamp)]) .success() @@ -86,7 +99,7 @@ pub async fn render_and_publish( Command::new("git") .arg("-C") - .arg(&render_repo) + .arg(&rendered.repo_dir) .arg("push") .arg("--force") .arg("--quiet") diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index bec1c64..dae74f2 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -166,11 +166,14 @@ impl Tree { &|_g, (_, element)| element.to_dot_node_attributes(None), ); - format!(r#"digraph {{ + format!( + r#"digraph {{ ranksep=1.2 {:?} }} -"#, dot) +"#, + dot + ) } pub fn to_mermaid(&self, repo_url: &str) -> String { diff --git a/techtree-manager/src/wiki.rs b/techtree-manager/src/wiki.rs index 0f1de44..9c48ee6 100644 --- a/techtree-manager/src/wiki.rs +++ b/techtree-manager/src/wiki.rs @@ -1,12 +1,29 @@ use anyhow::Context as _; use base64::prelude::*; -pub async fn update_wiki_overview( +pub async fn update_wiki_from_tree( ctx: &crate::Context, - new_body: String, + tree: &crate::tree::Tree, ) -> 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... - let _ = ctx.forgejo + let _ = ctx + .forgejo .repo_edit_wiki_page( &ctx.owner, &ctx.repo, From c1936c7ed20c219c0e7309b85cdfd894f28b4eed Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 20:37:10 +0200 Subject: [PATCH 30/63] manager: More cleanup --- techtree-manager/src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index a8b6ef1..11b7bcb 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -44,7 +44,7 @@ async fn run() -> anyhow::Result<()> { let token = std::env::var("GITHUB_TOKEN").context("Failed accessing GITHUB_TOKEN auth token")?; - let auth = forgejo_api::Auth::Token(&token); + let server_url = url::Url::parse( &std::env::var("GITHUB_SERVER_URL") .context("Failed reading GITHUB_SERVER_URL server url")?, @@ -61,7 +61,8 @@ async fn run() -> anyhow::Result<()> { repo_auth_url.set_username("forgejo-actions").unwrap(); repo_auth_url.set_password(Some(&token)).unwrap(); - let forgejo = Forgejo::new(auth, server_url).context("Could not create API access object")?; + let auth = forgejo_api::Auth::Token(&token); + let forgejo = Forgejo::new(auth, server_url).context("Could not create Forgejo API access object")?; let ctx = Context { forgejo, From 7762dbe80f69e27a9bbe0382e6c68dcb1566deac Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 21:28:21 +0200 Subject: [PATCH 31/63] README: Document how this tech tree works --- README.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/README.md b/README.md index 3ad60fd..2ca659c 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,143 @@ FAFO Technology Tree This repository tracks our "technology tree" — the steps towards our overarching goals. +A description of how this tech tree works can be found below. See +[Working with the Tech Tree](#working-with-the-tech-tree). + ## Full Technology Tree Below, you can see the full technology tree. The issues in this repository track the elements it contains. For better overview, check each issue for its relevant subtree. ![FAFO Tech Tree](https://git.fa-fo.de/rahix/techtree-poc/raw/branch/render/techtree.svg) + +## Working with the Tech Tree +Fundamentally, the tech tree is built from the issues in this repository and +their interdependencies. For elements to be reached, create new issues and +select one of the `Type/###` labels to declare what kind it is: + +- Type/**Equipment** 🔬 — A piece of machinery that we need to acquire or get + running. Mainly things that can be bought instead of built. + +- Type/**Process** ⚗️ — A process we need to achieve. This means we need to + become able to perform this process reliably. + +- Type/**Development** 🔩 — A device, software, or other thing we need to develop. + This is _engineering_ work; making use of existing knowledge to build + something useful. + +- Type/**Research** 🧪 — A topic that we can or must research to unlock future + capabilities. In contrast to _Development_ elements, we cannot make use of + prior art here. So less engineering and more _science_. + +#### Dependencies +Once created, add dependencies between issues to model their relationships. +The CI of this repository will automatically update the tech tree accordingly. +In addition, each issue gets a partial representation of the subtree of +elements directly related to it. You can quickly see what is still missing to +achieve a particular element. And also what next steps will be unlocked once +an element has been achieved. + +There are no distinct types of dependencies. This was a choice in the name of +keeping the model simple. All dependencies are hard requirements. But also +check the next section on more thoughts about this... + +#### Modelling Approach +Finding the right balance between model complexity and expressiveness is +tricky. Following are some guidelines for adding elements to this tech tree. + +All elements should have a few fundamental properties, to keep the model consistent: + +- Elements shall have a **clear and unambiguous acceptance criterion**. If + necessary, this can be elaborated on in the issue body. "SEM Imaging" leaves + open what scale we can image reliably. It may be sensible to add multiple + elements to model progress in such a domain. + +- Elements shall be **actionable**, in the sense that someone can put in effort to + achieve them. Having a good acceptance criterion does most of the heavy + lifting here. + +- Elements shall be **necessary** for our bigger visions. Ultimate elements + (elements that nothing depends on) should get special consideration in this + regard. If you have visions of your own, it is of course fine to make an + ultimate element for them. + +- Elements shall have a scope/granularity size that is appropriate for tracking + progress. We will need to figure this out as we go. + +Generally, our tech tree is a living object. It should be updated as we figure +out more elements to be tracked and their dependencies. + +One topic of particular interest are "path choices". We can either use +technology A or technology B to achieve element C: + +```mermaid +flowchart BT + classDef eoi fill:#fff, stroke:#000; + classDef dependant fill:#fff, stroke:#888, color:#888; + classDef dep_missing fill:#fcc, stroke:#800; + classDef dep_assigned fill:#ffa, stroke:#a50; + classDef dep_completed fill:#afa, stroke:#080; + 0:::dep_missing + 0["#1 | MISSING\nProcess\nTechnology A"] + 1:::dep_completed + 1["#2 | COMPLETED\nProcess\nTechnology B"] + 2:::eoi + 2["#3 | MISSING\nProcess\nElement C"] + 2 --> 0 + 2 --> 1 +``` + +Dependencies cannot express such choices. Instead, please follow this approach: + +- Initially, all potential path choice dependencies are added. The issue body + shall document the choice options and implications. + +- When we get closer to the element of interest, we make the choice of what + path to pursue. At this time, the other dependencies are dropped. The + tech-tree now only documents the path choice we have made. + +Another situation we will encounter is the need to use generic equipment for +specific purposes. If the specific purpose is non-trivial, it shall be +modelled as an intermediate _Process_ element in-between: + +```mermaid +flowchart BT + classDef eoi fill:#fff, stroke:#000; + classDef dependant fill:#fff, stroke:#888, color:#888; + classDef dep_missing fill:#fcc, stroke:#800; + classDef dep_assigned fill:#ffa, stroke:#a50; + classDef dep_completed fill:#afa, stroke:#080; + 0:::dep_missing + 0["#2 | MISSING\nProcess\nProcessing X using machine A"] + 1:::dep_completed + 1["#1 | COMPLETED\nEquipment\nGeneric Machine A"] + 2:::eoi + 2["#3 | MISSING\nProcess\nElement C"] + 2 --> 0 + 0 --> 1 +``` + + + +#### Element Status +The status of each element is determined as follows: + +- **MISSING** when the element has not yet been achieved. +- **ASSIGNED** when the issue has been assigned to someone. This means we are making progress! +- **COMPLETED** when the issue gets closed. Achievement unlocked! + +#### Important notes +There are a few gotchas that you should be aware of: + +- The CI will not automatically update when changing dependencies, + unfortunately. You can force a trigger by editing the issue body or quickly + adding and then removing the `Stale` label. + +- You cannot add dependencies on closed issues via the Forgejo interface. To + model such dependencies, temporarily reopen the dependency issue and close it + again after adding the relationship. + +- Issues that should no longer be a part of the techtree should either be + deleted (looses all tracking) or they can be made inert by removing the + `Type/###` label. From 3fae6a2b7ce2055ed9a3af6b9c86d0ee56fe0cb9 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 21:29:29 +0200 Subject: [PATCH 32/63] ci: Limit build jobs to main branch --- .forgejo/workflows/push.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.forgejo/workflows/push.yml b/.forgejo/workflows/push.yml index 733d119..60584c4 100644 --- a/.forgejo/workflows/push.yml +++ b/.forgejo/workflows/push.yml @@ -1,5 +1,8 @@ on: push: + branches: + - 'main' + - 'poc' jobs: build: From 8ae5ff6b81b10c96f58eba58010ca644cd0aff09 Mon Sep 17 00:00:00 2001 From: Rahix Date: Thu, 22 May 2025 21:50:10 +0200 Subject: [PATCH 33/63] README: Use live link of the rendered tech tree --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ca659c..45153fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Below, you can see the full technology tree. The issues in this repository track the elements it contains. For better overview, check each issue for its relevant subtree. -![FAFO Tech Tree](https://git.fa-fo.de/rahix/techtree-poc/raw/branch/render/techtree.svg) +![FAFO Tech Tree](https://git.fa-fo.de/fafo/techtree/raw/branch/render/techtree.svg) ## Working with the Tech Tree Fundamentally, the tech tree is built from the issues in this repository and From b484256246def428ae6907ff453083f81fd3d302 Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 23 May 2025 00:44:36 +0200 Subject: [PATCH 34/63] manager: Mark completion using a label Don't use the open/close state of the issues to mark their completion. This has friction with dependencies, because you can only add dependencies on open issues. Instead, issues are marked as completed using a "Completed" label now. --- README.md | 9 ++------- techtree-manager/src/collect.rs | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 45153fe..060fcc0 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ The status of each element is determined as follows: - **MISSING** when the element has not yet been achieved. - **ASSIGNED** when the issue has been assigned to someone. This means we are making progress! -- **COMPLETED** when the issue gets closed. Achievement unlocked! +- **COMPLETED** when the issue is labelled `Completed`. Achievement unlocked! #### Important notes There are a few gotchas that you should be aware of: @@ -136,10 +136,5 @@ There are a few gotchas that you should be aware of: unfortunately. You can force a trigger by editing the issue body or quickly adding and then removing the `Stale` label. -- You cannot add dependencies on closed issues via the Forgejo interface. To - model such dependencies, temporarily reopen the dependency issue and close it - again after adding the relationship. - - Issues that should no longer be a part of the techtree should either be - deleted (looses all tracking) or they can be made inert by removing the - `Type/###` label. + deleted (looses all tracking) or they can be made inert by closing them. diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 79b23af..8fa3654 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -77,10 +77,12 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result = issue + let labels = issue .labels .as_ref() - .context("Issue does not have any labels")? + .context("Issue does not have any labels")?; + + let ty_labels: Vec<_> = labels .iter() .filter_map(|l| l.name.as_deref()) .filter(|l| l.starts_with("Type/")) @@ -100,9 +102,15 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result { - if issue.assignee.is_some() + if has_completed_label { + crate::tree::ElementStatus::Completed + } else if issue.assignee.is_some() || issue .assignees .as_ref() @@ -114,7 +122,7 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result crate::tree::ElementStatus::Completed, + forgejo_api::structs::StateType::Closed => anyhow::bail!("Ignoring closed issue!"), }; Ok(crate::tree::Element { From d900bd80abbbab353dbb61ca3dbec9004e58eb4c Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 23 May 2025 00:46:15 +0200 Subject: [PATCH 35/63] manager: Fix fake event not pointing at the correct repo --- techtree-manager/src/event_meta.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/event_meta.rs b/techtree-manager/src/event_meta.rs index c6aafe6..bcc0d83 100644 --- a/techtree-manager/src/event_meta.rs +++ b/techtree-manager/src/event_meta.rs @@ -49,8 +49,8 @@ pub fn fake() -> IssueEventMeta { issue: IssueMeta { number: 1337, repository: RepoMeta { - name: "techtree-poc".to_owned(), - owner: "rahix".to_owned(), + name: "techtree".to_owned(), + owner: "fafo".to_owned(), }, }, } From 95991f47d406b58f368dbe377094a601142de397 Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 23 May 2025 01:19:28 +0200 Subject: [PATCH 36/63] Remove original tech tree dot file All information has been transcoded into the issue manager. --- tree.dot | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 tree.dot diff --git a/tree.dot b/tree.dot deleted file mode 100644 index 906d6d1..0000000 --- a/tree.dot +++ /dev/null @@ -1,52 +0,0 @@ -digraph { - wet_lab [label="Wet Lab"] - lapping [label="Lapping Machine"] - fiber_laser [label="Fiber Laser"] - rie [label="Reactive Ion Etching"] - plasma [label="Plasma Etching"] - - wet_lab -> decapping [label="HNO3/H2SO4"] - lapping -> decapping - fiber_laser -> decapping - plasma -> decapping - decapping [label="Decapping"] - - delayering [label="Delayering"] - fiber_laser -> delayering - lapping -> delayering - rie -> delayering - decapping -> delayering - wet_lab -> delayering [label="HNO3/HF"] - - optical_stage [label="Optical Microscope with X/Y motorized stage"] - - laser_fault_injection [label="Laser Fault Injection"] - decapping -> laser_fault_injection - optical_stage -> laser_fault_injection - - body_bias_injection [label="Body Bias Injection"] - decapping -> body_bias_injection - - sem_motorized_stage [label="SEM motorized stage"] - - chip_imaging [label="Simple Chip Imaging"] - delayering -> chip_imaging - sem_motorized_stage -> chip_imaging - - nanoprobing [label="SEM nanoprobing/live analysis"] - sem_motorized_stage -> nanoprobing - delayering -> nanoprobing - - sem_fault_injection [label="SEM Fault Injection"] - nanoprobing -> sem_fault_injection - - staining [label="DASH Stain"] - wet_lab -> staining [label="HNO3/HF"] - - full_chip_re [label="Full Chip RE"] - chip_imaging -> full_chip_re - staining -> full_chip_re - - microprobing [label="Microprobing"] - optical_stage -> microprobing -} From c08beba27b7a4a93753a60043aaf21bd87c6a33d Mon Sep 17 00:00:00 2001 From: Rahix Date: Sat, 24 May 2025 21:10:12 +0200 Subject: [PATCH 37/63] manager: Implement pagination for issue fetching Forgejo seems to limit requests to 50 issues server side, regardless of the limit we send in our request. This means we have to fetch each page separately regardless. --- techtree-manager/src/collect.rs | 45 ++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 8fa3654..c0034ed 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -2,23 +2,34 @@ use anyhow::Context as _; /// Read all issues to generate the full techtree pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result { - let issues = ctx - .forgejo - .issue_list_issues( - &ctx.owner, - &ctx.repo, - forgejo_api::structs::IssueListIssuesQuery { - // We also want the closed issues - state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), - // No pagination - limit: Some(10000), - // Only issues - r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues), - ..Default::default() - }, - ) - .await - .context("Failed fetching issue list")?; + let mut issues = vec![]; + for page in 1.. { + let new = ctx + .forgejo + .issue_list_issues( + &ctx.owner, + &ctx.repo, + forgejo_api::structs::IssueListIssuesQuery { + // We also want the closed issues + state: Some(forgejo_api::structs::IssueListIssuesQueryState::All), + // We cannot turn off pagination entirely, but let's set the limit as high as + // Forgejo lets us. + limit: Some(10000), + page: Some(page), + // Only issues + r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues), + ..Default::default() + }, + ) + .await + .with_context(|| format!("Failed fetching page {page} of the issue list"))?; + + if new.len() == 0 { + break; + } + + issues.extend(new); + } let mut tree = crate::tree::Tree::new(); let mut issue_numbers = Vec::new(); From 42ec50cb64712d9649e2c2d61a1af21249b20e52 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sat, 24 May 2025 21:16:29 +0200 Subject: [PATCH 38/63] manager: Add license --- techtree-manager/Cargo.toml | 3 + techtree-manager/LICENSE-APACHE | 201 ++++++++++++++++++++++++++++++++ techtree-manager/LICENSE-MIT | 23 ++++ 3 files changed, 227 insertions(+) create mode 100644 techtree-manager/LICENSE-APACHE create mode 100644 techtree-manager/LICENSE-MIT diff --git a/techtree-manager/Cargo.toml b/techtree-manager/Cargo.toml index a2e6f82..4625209 100644 --- a/techtree-manager/Cargo.toml +++ b/techtree-manager/Cargo.toml @@ -2,6 +2,9 @@ name = "techtree-manager" version = "0.1.0" edition = "2024" +authors = ["rahix "] +license = "MIT OR Apache-2.0" +publish = false [dependencies] anyhow = "1.0.98" diff --git a/techtree-manager/LICENSE-APACHE b/techtree-manager/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/techtree-manager/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/techtree-manager/LICENSE-MIT b/techtree-manager/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/techtree-manager/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. From cc05ba7407810a357cdc5e4c9adb3236bb2b2c48 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 25 May 2025 03:42:59 +0200 Subject: [PATCH 39/63] manager: Simplify very complex mermaid graphs Forgejo has a limit of 5000 characters per mermaid graph. We started hitting this for some issues. Regenerate a somewhat simplified version of the subtree in this case which hopefully does not hit the same limit again. --- techtree-manager/src/issue.rs | 26 +++++++++++++++++++++----- techtree-manager/src/tree.rs | 28 +++++++++++++++++----------- techtree-manager/src/wiki.rs | 2 +- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index 3fd50c7..510a03b 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -111,17 +111,33 @@ pub async fn update_bot_comment_from_subtree( subtree: &crate::tree::Subtree<'_>, hash: &str, ) -> anyhow::Result<()> { - let mermaid = subtree.to_mermaid(&ctx.repo_url.to_string()); + let mut mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), false); - let full_text = format!( - r##"## Partial Techtree + // When the mermaid graph gets too big, regenerate a simplified version. + if mermaid.len() > 4990 { + log::info!("Mermaid graph is too big, generating simplified version..."); + mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), true); + } + + let full_text = if mermaid.len() > 4990 { + format!( + r##"## Partial Techtree +_Sorry, the partial techtree is still too big to be displayed for this element..._ + +Digest: {hash}; Last Updated: {timestamp}"##, + timestamp = ctx.timestamp, + ) + } else { + format!( + r##"## Partial Techtree ```mermaid {mermaid} ``` Digest: {hash}; Last Updated: {timestamp}"##, - timestamp = ctx.timestamp, - ); + timestamp = ctx.timestamp, + ) + }; update_bot_comment(&ctx, id, full_text).await?; diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index dae74f2..207515c 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -51,6 +51,7 @@ impl Element { index: ElementIndex, role: Option, repo_url: &str, + simple: bool, ) -> String { let Element { issue_number, @@ -59,9 +60,13 @@ impl Element { status, } = self; - let label = format!( - r##"#{issue_number} | {status}\n{ty}\n{description}"## - ); + let label = if simple { + format!(r##"#{issue_number} | {status}\n{ty}\n{description}"##) + } else { + format!( + r##"#{issue_number} | {status}\n{ty}\n{description}"## + ) + }; let class = match (role, status) { (Some(SubtreeElementRole::ElementOfInterest), _) => "eoi", @@ -176,13 +181,13 @@ impl Tree { ) } - pub fn to_mermaid(&self, repo_url: &str) -> String { + pub fn to_mermaid(&self, repo_url: &str, simple: bool) -> String { let mut mermaid = String::new(); mermaid.push_str("flowchart BT\n"); mermaid.push_str(&mermaid_classes()); for index in self.graph.node_indices() { - mermaid.push_str(&self.graph[index].to_mermaid_node(index, None, repo_url)); + mermaid.push_str(&self.graph[index].to_mermaid_node(index, None, repo_url, simple)); mermaid.push_str("\n"); } @@ -300,18 +305,19 @@ impl<'a> Subtree<'a> { format!("{:?}", dot) } - pub fn to_mermaid(&self, repo_url: &str) -> String { + pub fn to_mermaid(&self, repo_url: &str, simple: bool) -> String { let mut mermaid = String::new(); mermaid.push_str("flowchart BT\n"); mermaid.push_str(&mermaid_classes()); for index in self.graph.node_indices() { let node = &self.graph[index]; - mermaid.push_str( - &node - .element - .to_mermaid_node(index, Some(node.role), repo_url), - ); + mermaid.push_str(&node.element.to_mermaid_node( + index, + Some(node.role), + repo_url, + simple, + )); mermaid.push_str("\n"); } diff --git a/techtree-manager/src/wiki.rs b/techtree-manager/src/wiki.rs index 9c48ee6..e61f931 100644 --- a/techtree-manager/src/wiki.rs +++ b/techtree-manager/src/wiki.rs @@ -5,7 +5,7 @@ pub async fn update_wiki_from_tree( ctx: &crate::Context, tree: &crate::tree::Tree, ) -> anyhow::Result<()> { - let mermaid = tree.to_mermaid(&ctx.repo_url.to_string()); + let mermaid = tree.to_mermaid(&ctx.repo_url.to_string(), false); let wiki_text = format!( r##"This page is automatically updated to show the latest and greatest FAFO techtree: From 15726d9ee8a1bc1fb54eb90731e55463be6c96d8 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 25 May 2025 15:33:24 +0200 Subject: [PATCH 40/63] manager: Apply rustfmt --- techtree-manager/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 11b7bcb..6303643 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -62,7 +62,8 @@ async fn run() -> anyhow::Result<()> { repo_auth_url.set_password(Some(&token)).unwrap(); let auth = forgejo_api::Auth::Token(&token); - let forgejo = Forgejo::new(auth, server_url).context("Could not create Forgejo API access object")?; + let forgejo = + Forgejo::new(auth, server_url).context("Could not create Forgejo API access object")?; let ctx = Context { forgejo, From 1cefb058bf14ea83d7c74b343be6d3f745a07083 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 25 May 2025 15:32:12 +0200 Subject: [PATCH 41/63] manager: Fix mermaid style on dark Forgejo theme Forgejo seems to automatically choose a light font color for mermaid graphs when the site-theme is dark. This conflicts with the background colors of our mermaid nodes. Explicitly force a dark font color to ensure the text is also readable on dark Forgejo. --- techtree-manager/src/tree.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 207515c..cf693cc 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -85,11 +85,11 @@ impl Element { fn mermaid_classes() -> String { r##" - classDef eoi fill:#fff, stroke:#000; + classDef eoi fill:#fff, stroke:#000, color:#000; classDef dependant fill:#fff, stroke:#888, color:#888; - classDef dep_missing fill:#fcc, stroke:#800; - classDef dep_assigned fill:#ffa, stroke:#a50; - classDef dep_completed fill:#afa, stroke:#080; + classDef dep_missing fill:#fcc, stroke:#800, color:#000; + classDef dep_assigned fill:#ffa, stroke:#a50, color:#000; + classDef dep_completed fill:#afa, stroke:#080, color:#000; "## .to_owned() } From e188e1863755f3add216e7b54eb698d23c2c0da1 Mon Sep 17 00:00:00 2001 From: Rahix Date: Wed, 28 May 2025 02:54:31 +0200 Subject: [PATCH 42/63] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 060fcc0..e3c1051 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A description of how this tech tree works can be found below. See ## Full Technology Tree Below, you can see the full technology tree. The issues in this repository -track the elements it contains. For better overview, check each issue for its -relevant subtree. +track the tree's elements. For a better overview, check each issue for a +filtered subtree that contains just the elements related to it. ![FAFO Tech Tree](https://git.fa-fo.de/fafo/techtree/raw/branch/render/techtree.svg) From 511a4467a3953cad48352523769d0b8a7a92bbc1 Mon Sep 17 00:00:00 2001 From: Rahix Date: Wed, 28 May 2025 03:01:56 +0200 Subject: [PATCH 43/63] Update README Also update the mermaid classes that are used in the README. Fixes: 1cefb058bf14 ("manager: Fix mermaid style on dark Forgejo theme") --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e3c1051..8adad5f 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,11 @@ technology A or technology B to achieve element C: ```mermaid flowchart BT - classDef eoi fill:#fff, stroke:#000; + classDef eoi fill:#fff, stroke:#000, color:#000; classDef dependant fill:#fff, stroke:#888, color:#888; - classDef dep_missing fill:#fcc, stroke:#800; - classDef dep_assigned fill:#ffa, stroke:#a50; - classDef dep_completed fill:#afa, stroke:#080; + classDef dep_missing fill:#fcc, stroke:#800, color:#000; + classDef dep_assigned fill:#ffa, stroke:#a50, color:#000; + classDef dep_completed fill:#afa, stroke:#080, color:#000; 0:::dep_missing 0["#1 | MISSING\nProcess\nTechnology A"] 1:::dep_completed @@ -105,11 +105,11 @@ modelled as an intermediate _Process_ element in-between: ```mermaid flowchart BT - classDef eoi fill:#fff, stroke:#000; + classDef eoi fill:#fff, stroke:#000, color:#000; classDef dependant fill:#fff, stroke:#888, color:#888; - classDef dep_missing fill:#fcc, stroke:#800; - classDef dep_assigned fill:#ffa, stroke:#a50; - classDef dep_completed fill:#afa, stroke:#080; + classDef dep_missing fill:#fcc, stroke:#800, color:#000; + classDef dep_assigned fill:#ffa, stroke:#a50, color:#000; + classDef dep_completed fill:#afa, stroke:#080, color:#000; 0:::dep_missing 0["#2 | MISSING\nProcess\nProcessing X using machine A"] 1:::dep_completed From 96bb2540ba1c7c95014a8e5c34b1a0c09759fa40 Mon Sep 17 00:00:00 2001 From: Rahix Date: Wed, 28 May 2025 11:46:24 +0200 Subject: [PATCH 44/63] Update README Use media link for the techtree image. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8adad5f..0ee25a1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Below, you can see the full technology tree. The issues in this repository track the tree's elements. For a better overview, check each issue for a filtered subtree that contains just the elements related to it. -![FAFO Tech Tree](https://git.fa-fo.de/fafo/techtree/raw/branch/render/techtree.svg) +![FAFO Tech Tree](https://git.fa-fo.de/fafo/techtree/media/branch/render/techtree.svg) ## Working with the Tech Tree Fundamentally, the tech tree is built from the issues in this repository and From d4b8241808393747d09894812bb792cdcbf65cff Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 30 May 2025 21:35:00 +0200 Subject: [PATCH 45/63] Allow running the manager without a token This is useful for local testing. --- techtree-manager/src/main.rs | 48 ++++++++++++++++++++++++---------- techtree-manager/src/render.rs | 7 ++++- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 6303643..bfb830a 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, } @@ -42,13 +42,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 +59,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 +113,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 +130,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")?; From 92e8ea066f3840a05ef7ab41041a57b4dce258e1 Mon Sep 17 00:00:00 2001 From: Rahix Date: Mon, 28 Jul 2025 15:28:46 +0200 Subject: [PATCH 46/63] 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. --- techtree-manager/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index bfb830a..9d6caff 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -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!( From 1f889245afb5fec6d31943e1f503a4edb725602a Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 30 May 2025 21:48:07 +0200 Subject: [PATCH 47/63] Make tree serde-serializable --- techtree-manager/Cargo.lock | 1 + techtree-manager/Cargo.toml | 2 +- techtree-manager/src/tree.rs | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) 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/tree.rs b/techtree-manager/src/tree.rs index cf693cc..0574f73 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, } From 95194149291fd138cc0fe14001bb4d068de73cde Mon Sep 17 00:00:00 2001 From: Rahix Date: Mon, 28 Jul 2025 15:38:36 +0200 Subject: [PATCH 48/63] Push all ultimate elements to the bottom of the graph 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. --- techtree-manager/src/tree.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 0574f73..f87873c 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -160,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, @@ -173,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}; }} {:?} }} "#, From 22c489f983a9719857b7e4e54de5715e702f0023 Mon Sep 17 00:00:00 2001 From: Rahix Date: Mon, 28 Jul 2025 16:11:50 +0200 Subject: [PATCH 49/63] Slightly highlight root elements In the dot representation of the techtree, highlight root elements with a slightly different shade so they are easy to pick out. This tells people at a glance what topics they can immediately start working on :) --- techtree-manager/src/tree.rs | 57 ++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index f87873c..bc3e343 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -15,8 +15,20 @@ pub struct Element { pub status: ElementStatus, } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ElementRole { + Root, + Ultimate, + Intermediate, + Disjoint, +} + impl Element { - fn to_dot_node_attributes(&self, role: Option) -> String { + fn to_dot_node_attributes( + &self, + role: Option, + subtree_role: Option, + ) -> String { let Element { issue_number, description, @@ -31,10 +43,18 @@ impl Element { )); attributes.push(r#"shape = "record""#.to_owned()); - let (color, background) = match (role, status) { + let (color, background) = match (subtree_role, status) { (Some(SubtreeElementRole::ElementOfInterest), _) => ("black", "white"), (Some(SubtreeElementRole::Dependant), _) => ("gray", "gray"), - (_, ElementStatus::Missing) => ("#800", "#fcc"), + (_, ElementStatus::Missing) => ( + "#800", + // Highlight root elements + if role == Some(ElementRole::Root) { + "#ffddc1" + } else { + "#fcc" + }, + ), (_, ElementStatus::Assigned) => ("#a50", "#ffa"), (_, ElementStatus::Completed) => ("#080", "#afa"), }; @@ -170,7 +190,30 @@ impl Tree { }) } + pub fn get_element_role(&self, element: ElementIndex) -> ElementRole { + let has_dependencies = self + .graph + .neighbors_directed(element, petgraph::Direction::Outgoing) + .next() + .is_some(); + let has_dependants = self + .graph + .neighbors_directed(element, petgraph::Direction::Incoming) + .next() + .is_some(); + + match (has_dependencies, has_dependants) { + (false, true) => ElementRole::Root, + (true, false) => ElementRole::Ultimate, + (true, true) => ElementRole::Intermediate, + (false, false) => ElementRole::Disjoint, + } + } + pub fn to_dot(&self) -> String { + let to_node_attributes = |_g, (id, element): (ElementIndex, &Element)| { + element.to_dot_node_attributes(Some(self.get_element_role(id)), None) + }; let dot = petgraph::dot::Dot::with_attr_getters( &self.graph, &[ @@ -180,7 +223,7 @@ impl Tree { petgraph::dot::Config::GraphContentOnly, ], &|_g, _edge_id| "".to_string(), - &|_g, (_, element)| element.to_dot_node_attributes(None), + &to_node_attributes, ); let ultimate_elements: Vec<_> = self @@ -318,7 +361,11 @@ impl<'a> Subtree<'a> { petgraph::dot::Config::RankDir(petgraph::dot::RankDir::BT), ], &|_g, _edge_id| "".to_string(), - &|_g, (_, element)| element.element.to_dot_node_attributes(Some(element.role)), + &|_g, (_, element)| { + element + .element + .to_dot_node_attributes(None, Some(element.role)) + }, ); format!("{:?}", dot) From 926d546c4233fd4703477a6276a9616ebcb32046 Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 26 Sep 2025 00:56:56 +0200 Subject: [PATCH 50/63] Properly escape element descriptions Make sure dot is happy with the provided labels by correctly escaping special characters inside the element descriptions. --- techtree-manager/src/tree.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index bc3e343..6a81bce 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -38,6 +38,13 @@ impl Element { let mut attributes = Vec::new(); + let description = description + .replace("<", "<") + .replace(">", ">") + .replace("&", "&") + .replace("'", "'") + .replace("\"", """); + attributes.push(format!( r##"label = <{{{{#{issue_number} | {status}}}|{ty}|{description}}}>"## )); From da085da38fdbf24bfbb5929a40f58fffd71053a5 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 5 Oct 2025 18:43:52 +0200 Subject: [PATCH 51/63] Fix newlines in mermaid diagrams Apparently an upgrade of our Forgejo version lead to `\n` no longer being rendered as a newline in mermaid. Replace with `
` which does the right thing. --- techtree-manager/src/tree.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index 6a81bce..fef523d 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -88,10 +88,10 @@ impl Element { } = self; let label = if simple { - format!(r##"#{issue_number} | {status}\n{ty}\n{description}"##) + format!(r##"#{issue_number} | {status}
{ty}
{description}"##) } else { format!( - r##"#{issue_number} | {status}\n{ty}\n{description}"## + r##"#{issue_number} | {status}
{ty}
{description}"## ) }; From 77d7d3aef6079515ce4c6345f631d24f60588291 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 5 Oct 2025 18:59:47 +0200 Subject: [PATCH 52/63] Introduce a digest epoch To add a mechanism for updating all issue comments after a code change, introduce a HASH_EPOCH constant which gets mixed into the stable hash for each issue. Changing this value will force all issue comments to be updated. --- techtree-manager/src/tree.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index fef523d..db6a93f 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use petgraph::visit::EdgeRef as _; +const HASH_EPOCH: u32 = 0x00000001; + /// Element in the techtree #[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize)] pub struct Element { @@ -420,7 +422,7 @@ impl<'a> Subtree<'a> { .collect(); edges.sort(); - let json_data = serde_json::ser::to_string(&(nodes, edges)).unwrap(); + let json_data = serde_json::ser::to_string(&(nodes, edges, HASH_EPOCH)).unwrap(); sha256::digest(json_data) } } From 1451b4291290faa32d788845a85998ed01173b33 Mon Sep 17 00:00:00 2001 From: Rahix Date: Sun, 12 Oct 2025 09:58:42 +0200 Subject: [PATCH 53/63] README: Fix mermaid newlines --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ee25a1..af1bdae 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,11 @@ flowchart BT classDef dep_assigned fill:#ffa, stroke:#a50, color:#000; classDef dep_completed fill:#afa, stroke:#080, color:#000; 0:::dep_missing - 0["#1 | MISSING\nProcess\nTechnology A"] + 0["#1 | MISSING
Process
Technology A"] 1:::dep_completed - 1["#2 | COMPLETED\nProcess\nTechnology B"] + 1["#2 | COMPLETED
Process
Technology B"] 2:::eoi - 2["#3 | MISSING\nProcess\nElement C"] + 2["#3 | MISSING
Process
Element C"] 2 --> 0 2 --> 1 ``` @@ -111,11 +111,11 @@ flowchart BT classDef dep_assigned fill:#ffa, stroke:#a50, color:#000; classDef dep_completed fill:#afa, stroke:#080, color:#000; 0:::dep_missing - 0["#2 | MISSING\nProcess\nProcessing X using machine A"] + 0["#2 | MISSING
Process
Processing X using machine A"] 1:::dep_completed - 1["#1 | COMPLETED\nEquipment\nGeneric Machine A"] + 1["#1 | COMPLETED
Equipment
Generic Machine A"] 2:::eoi - 2["#3 | MISSING\nProcess\nElement C"] + 2["#3 | MISSING
Process
Element C"] 2 --> 0 0 --> 1 ``` From c8b848d89bd397ea0c4132b7aeb2417221f7bdd5 Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 17 Oct 2025 12:46:14 +0200 Subject: [PATCH 54/63] Ignore external issue dependencies Issues in forgejo can also depend on issues from other repositories. Ignore such external dependencies when building the techtree. --- techtree-manager/src/collect.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index c0034ed..b02afca 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -68,6 +68,21 @@ pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result Date: Fri, 17 Oct 2025 12:50:31 +0200 Subject: [PATCH 55/63] Make logs less noisy Don't emit log messages about issues with no changes. --- techtree-manager/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index 9d6caff..b593511 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -146,7 +146,7 @@ async fn run() -> anyhow::Result<()> { .with_context(|| format!("Failed to find or make bot comment in issue #{issue}"))?; if bot_comment.body.contains(&hash) { - log::info!("Issue #{issue} is up-to-date, not editing comment."); + 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}"))?; From 983ceb2076a74657695bb51e88dbf849eaef055c Mon Sep 17 00:00:00 2001 From: Rahix Date: Fri, 17 Oct 2025 12:55:06 +0200 Subject: [PATCH 56/63] Be quiet about the techtree update commit --- techtree-manager/src/render.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index f9ebe81..d8fbcd8 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -93,6 +93,7 @@ pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::R .arg("-C") .arg(&rendered.repo_dir) .arg("commit") + .arg("--quiet") .args(["-m", &format!("Updated techtree at {}", ctx.timestamp)]) .success() .context("Failed to add generated graph files to git index")?; From b66e8c8d9c1d9367df61e14f6452d6e154e9536b Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 15:29:21 +0100 Subject: [PATCH 57/63] Replace dirty unwrap() with expect("TODO") This makes it obvious where better error handling is still needed. --- techtree-manager/src/collect.rs | 4 ++-- techtree-manager/src/issue.rs | 8 ++++---- techtree-manager/src/main.rs | 8 ++++---- techtree-manager/src/render.rs | 14 ++++++++++++-- techtree-manager/src/tree.rs | 6 +++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index b02afca..7d1dcb8 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -123,9 +123,9 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result anyh .iter() .filter(|l| l.name.as_deref() == Some("Stale")) .next() - .map(|l| l.id.unwrap()); + .map(|l| l.id.expect("TODO")); if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index b593511..d9e2f81 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -60,12 +60,12 @@ async fn run() -> anyhow::Result<()> { "{}/{}", meta.issue.repository.owner, meta.issue.repository.name )) - .unwrap(); + .expect("TODO"); 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.set_username("forgejo-actions").expect("TODO"); + repo_auth_url.set_password(Some(&token)).expect("TODO"); repo_auth_url }); @@ -138,7 +138,7 @@ async fn run() -> anyhow::Result<()> { } 'issues: for issue in tree.iter_issues() { - let subtree = tree.subtree_for_issue(issue).unwrap(); + let subtree = tree.subtree_for_issue(issue).expect("TODO"); let hash = subtree.stable_hash(); let (bot_comment, _is_new) = issue::find_or_make_bot_comment(&ctx, issue) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index d8fbcd8..09f0e4b 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -84,8 +84,18 @@ pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::R .arg("-C") .arg(&rendered.repo_dir) .arg("add") - .arg(rendered.dot_file.strip_prefix(&rendered.repo_dir).unwrap()) - .arg(rendered.svg_file.strip_prefix(&rendered.repo_dir).unwrap()) + .arg( + rendered + .dot_file + .strip_prefix(&rendered.repo_dir) + .expect("TODO"), + ) + .arg( + rendered + .svg_file + .strip_prefix(&rendered.repo_dir) + .expect("TODO"), + ) .success() .context("Failed to add generated graph files to git index")?; diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index db6a93f..e3911fc 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -165,8 +165,8 @@ impl Tree { } pub fn add_dependency_by_issue_number(&mut self, dependant: u64, dependency: u64) { - let a = self.find_element_by_issue_number(dependant).unwrap(); - let b = self.find_element_by_issue_number(dependency).unwrap(); + let a = self.find_element_by_issue_number(dependant).expect("TODO"); + let b = self.find_element_by_issue_number(dependency).expect("TODO"); self.graph.add_edge(a, b, ()); } @@ -422,7 +422,7 @@ impl<'a> Subtree<'a> { .collect(); edges.sort(); - let json_data = serde_json::ser::to_string(&(nodes, edges, HASH_EPOCH)).unwrap(); + let json_data = serde_json::ser::to_string(&(nodes, edges, HASH_EPOCH)).expect("TODO"); sha256::digest(json_data) } } From 6d6fffcb4a35e51644df0d62363ec6a5dd3383b3 Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 15:33:57 +0100 Subject: [PATCH 58/63] Reduce error potentials in type label parsing --- techtree-manager/src/collect.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/techtree-manager/src/collect.rs b/techtree-manager/src/collect.rs index 7d1dcb8..ef3eca1 100644 --- a/techtree-manager/src/collect.rs +++ b/techtree-manager/src/collect.rs @@ -111,22 +111,18 @@ fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result = labels .iter() .filter_map(|l| l.name.as_deref()) - .filter(|l| l.starts_with("Type/")) + .filter_map(|l| l.strip_prefix("Type/")) .collect(); - if ty_labels.len() == 0 { - anyhow::bail!("Issue #{issue_number} has no type label!"); - } - if ty_labels.len() > 1 { - anyhow::bail!("Issue #{issue_number} has more than one type label!"); - } - - let ty = ty_labels - .first() - .expect("TODO") - .strip_prefix("Type/") - .expect("TODO") - .to_owned(); + let ty = match &ty_labels[..] { + [ty] => ty.to_string(), + [] => { + anyhow::bail!("Issue #{issue_number} has no type label!"); + } + [..] => { + anyhow::bail!("Issue #{issue_number} has more than one type label!"); + } + }; let has_completed_label = labels .iter() From 3d73094192f6cd995b5e44857fa2ca7e34472145 Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 15:44:29 +0100 Subject: [PATCH 59/63] Refactor rendering code to prevent illegal states Store the rendered tree information such that it is impossible to represent a set of data that is incorrect. --- techtree-manager/src/render.rs | 60 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/techtree-manager/src/render.rs b/techtree-manager/src/render.rs index 09f0e4b..f18bebb 100644 --- a/techtree-manager/src/render.rs +++ b/techtree-manager/src/render.rs @@ -16,9 +16,19 @@ impl SuccessExt for Command { } pub struct RenderedTree { - repo_dir: std::path::PathBuf, - dot_file: std::path::PathBuf, - svg_file: std::path::PathBuf, + pub repo_dir: std::path::PathBuf, + pub dot_file_name: std::ffi::OsString, + pub svg_file_name: std::ffi::OsString, +} + +impl RenderedTree { + pub fn dot_file(&self) -> std::path::PathBuf { + self.repo_dir.join(&self.dot_file_name) + } + + pub fn svg_file(&self) -> std::path::PathBuf { + self.repo_dir.join(&self.svg_file_name) + } } pub async fn render( @@ -27,33 +37,33 @@ pub async fn render( ) -> anyhow::Result { 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")?; + let info = RenderedTree { + repo_dir, + dot_file_name: "techtree.dot".into(), + svg_file_name: "techtree.svg".into(), + }; + + if info.repo_dir.is_dir() { + log::info!("Found old {:?} repository, removing...", info.repo_dir); + std::fs::remove_dir_all(&info.repo_dir) + .context("Failed to remove stale render repository")?; } - std::fs::create_dir(&repo_dir).context("Failed creating directory for rendered graph")?; - - let dot_file = repo_dir.join("techtree.dot"); - let svg_file = repo_dir.join("techtree.svg"); + std::fs::create_dir(&info.repo_dir).context("Failed creating directory for rendered graph")?; let dot_source = tree.to_dot(); - std::fs::write(&dot_file, dot_source.as_bytes()) + std::fs::write(&info.dot_file(), dot_source.as_bytes()) .context("Failed to write `dot` source file")?; Command::new("dot") .args(["-T", "svg"]) .arg("-o") - .arg(&svg_file) - .arg(&dot_file) + .arg(&info.svg_file()) + .arg(&info.dot_file()) .success() .context("Failed to generate svg graph from dot source")?; - Ok(RenderedTree { - repo_dir, - dot_file, - svg_file, - }) + Ok(info) } pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::Result<()> { @@ -84,18 +94,8 @@ pub async fn publish(ctx: &crate::Context, rendered: &RenderedTree) -> anyhow::R .arg("-C") .arg(&rendered.repo_dir) .arg("add") - .arg( - rendered - .dot_file - .strip_prefix(&rendered.repo_dir) - .expect("TODO"), - ) - .arg( - rendered - .svg_file - .strip_prefix(&rendered.repo_dir) - .expect("TODO"), - ) + .arg(&rendered.dot_file_name) + .arg(&rendered.svg_file_name) .success() .context("Failed to add generated graph files to git index")?; From d10b164b61d772d2e3ad6a3dc3c1e1601dc1feb0 Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 16:11:13 +0100 Subject: [PATCH 60/63] Properly catch errors during URL building --- techtree-manager/src/main.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index d9e2f81..cba851d 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -60,14 +60,21 @@ async fn run() -> anyhow::Result<()> { "{}/{}", meta.issue.repository.owner, meta.issue.repository.name )) - .expect("TODO"); + .with_context(|| { + format!("failed building repository URL from GITHUB_SERVER_URL: {server_url}") + })?; - let repo_auth_url = token.as_ref().map(|token| { - let mut repo_auth_url = repo_url.clone(); - repo_auth_url.set_username("forgejo-actions").expect("TODO"); - repo_auth_url.set_password(Some(&token)).expect("TODO"); - repo_auth_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) From 83a610134c3205002a8d299a520cd9bc6af05f2a Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 16:18:48 +0100 Subject: [PATCH 61/63] Add better panic messages where relevant --- techtree-manager/src/main.rs | 2 +- techtree-manager/src/tree.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/techtree-manager/src/main.rs b/techtree-manager/src/main.rs index cba851d..eda6053 100644 --- a/techtree-manager/src/main.rs +++ b/techtree-manager/src/main.rs @@ -145,7 +145,7 @@ async fn run() -> anyhow::Result<()> { } 'issues: for issue in tree.iter_issues() { - let subtree = tree.subtree_for_issue(issue).expect("TODO"); + 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) diff --git a/techtree-manager/src/tree.rs b/techtree-manager/src/tree.rs index e3911fc..3ec271f 100644 --- a/techtree-manager/src/tree.rs +++ b/techtree-manager/src/tree.rs @@ -165,8 +165,12 @@ impl Tree { } pub fn add_dependency_by_issue_number(&mut self, dependant: u64, dependency: u64) { - let a = self.find_element_by_issue_number(dependant).expect("TODO"); - let b = self.find_element_by_issue_number(dependency).expect("TODO"); + let a = self + .find_element_by_issue_number(dependant) + .expect("dependant element does not exist"); + let b = self + .find_element_by_issue_number(dependency) + .expect("dependency element does not exist"); self.graph.add_edge(a, b, ()); } @@ -422,7 +426,8 @@ impl<'a> Subtree<'a> { .collect(); edges.sort(); - let json_data = serde_json::ser::to_string(&(nodes, edges, HASH_EPOCH)).expect("TODO"); + let json_data = serde_json::ser::to_string(&(nodes, edges, HASH_EPOCH)) + .expect("serialization for a stable hash failed"); sha256::digest(json_data) } } From c11026c5be5d7447110ea8dcdc533d40b5c5f39c Mon Sep 17 00:00:00 2001 From: Rahix Date: Tue, 9 Dec 2025 16:19:21 +0100 Subject: [PATCH 62/63] Better handling of id errors from issue API calls --- techtree-manager/src/issue.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/techtree-manager/src/issue.rs b/techtree-manager/src/issue.rs index 9777c43..97cf9d9 100644 --- a/techtree-manager/src/issue.rs +++ b/techtree-manager/src/issue.rs @@ -28,7 +28,7 @@ pub async fn make_bot_comment( .await?; Ok(BotCommentInfo { - id: res.id.expect("TODO"), + id: res.id.context("Missing id for the bot comment")?, body: initial_message.to_owned(), }) } @@ -55,12 +55,16 @@ pub async fn find_bot_comment( let maybe_bot_comment = comments .iter() .rev() - .find(|comment| comment.user.as_ref().expect("TODO").id == Some(-2)); + .find(|comment| comment.user.as_ref().and_then(|u| u.id) == Some(-2)); - Ok(maybe_bot_comment.map(|c| BotCommentInfo { - body: c.body.clone().unwrap_or("".to_owned()), - id: c.id.expect("TODO"), - })) + Ok(maybe_bot_comment + .map(|c| -> anyhow::Result<_> { + Ok(BotCommentInfo { + body: c.body.clone().unwrap_or("".to_owned()), + id: c.id.context("Missing id for the bot comment")?, + }) + }) + .transpose()?) } /// Find existing bot comment or create a new one. @@ -155,7 +159,8 @@ pub async fn remove_stale_label(ctx: &crate::Context, issue_number: u64) -> anyh .iter() .filter(|l| l.name.as_deref() == Some("Stale")) .next() - .map(|l| l.id.expect("TODO")); + .map(|l| l.id.ok_or(anyhow::anyhow!("`Stale` label has no ID"))) + .transpose()?; if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); From 8f0f52a402c6d3e30428194c1389983b012c7352 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Tue, 9 Dec 2025 16:51:17 +0100 Subject: [PATCH 63/63] Updated techtree at 2025-12-09 16:51:08 --- techtree.dot | 278 ++++++ techtree.svg | 2322 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2600 insertions(+) create mode 100644 techtree.dot create mode 100644 techtree.svg diff --git a/techtree.dot b/techtree.dot new file mode 100644 index 0000000..18151c7 --- /dev/null +++ b/techtree.dot @@ -0,0 +1,278 @@ +digraph { + ranksep=1.2 + { rank=same; 0; 1; 2; 3; 4; 9; 25; 50; } + rankdir="BT" + 0 [ label = <{{#115 | MISSING}|Process|Semiconductor Manufacturing}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 1 [ label = <{{#114 | MISSING}|Process|Semiconductor & Electronics Analysis}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 2 [ label = <{{#113 | MISSING}|Process|Science Communication}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 3 [ label = <{{#112 | MISSING}|Process|Nanoscale Imaging}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 4 [ label = <{{#111 | MISSING}|Process|LIN supply}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 5 [ label = <{{#110 | ASSIGNED}|Equipment|Local NAS/server}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 6 [ label = <{{#109 | MISSING}|Equipment|Environmental Chamber}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 7 [ label = <{{#108 | MISSING}|Development|Piezo Driver}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 8 [ label = <{{#107 | MISSING}|Process|EMI Precompliance Testing}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 9 [ label = <{{#106 | MISSING}|Process|Electronics Distribution}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 10 [ label = <{{#105 | ASSIGNED}|Process|Create a Backlit LCD Device}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 11 [ label = <{{#104 | ASSIGNED}|Process|Electroluminescence Foil}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 12 [ label = <{{#103 | MISSING}|Process|Create a Matrix LCD Device}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 13 [ label = <{{#102 | ASSIGNED}|Research|Acquire Liquid Crystal Fluid}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 14 [ label = <{{#101 | MISSING}|Process|Chemical Vapor Deposition}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 15 [ label = <{{#100 | MISSING}|Process|Etch fractals on wafers}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 16 [ label = <{{#99 | ASSIGNED}|Process|Create an LCD Device}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 17 [ label = <{{#98 | ASSIGNED}|Development|Gridfinity-compatible JEDEC IC holders}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 18 [ label = <{{#97 | COMPLETED}|Equipment|PCB Rework Microscope}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 19 [ label = <{{#96 | COMPLETED}|Equipment|Soldering Station}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 20 [ label = <{{#95 | MISSING}|Equipment|Thermal Camera}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 21 [ label = <{{#94 | ASSIGNED}|Process|PCB Reverse Engineering}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 22 [ label = <{{#93 | COMPLETED}|Equipment|HEPA Fume Extractor}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 23 [ label = <{{#92 | COMPLETED}|Equipment|Miscellaneous Tools for BGA Rework}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 24 [ label = <{{#91 | COMPLETED}|Equipment|Hot Air Rework Station}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 25 [ label = <{{#90 | ASSIGNED}|Process|PCB Repair & Rework}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 26 [ label = <{{#89 | ASSIGNED}|Process|PCB Delayering}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 27 [ label = <{{#88 | MISSING}|Development|Piezo Slider Actuators}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 28 [ label = <{{#87 | MISSING}|Equipment|Diode Laser}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 29 [ label = <{{#86 | ASSIGNED}|Development|Optomechanical System}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 30 [ label = <{{#85 | ASSIGNED}|Development|Laser Diode Driver/Controller}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 31 [ label = <{{#84 | MISSING}|Equipment|Stabilized Helium-Neon Laser}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 32 [ label = <{{#83 | MISSING}|Equipment|External Cavity Diode Laser (ECDL)}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 33 [ label = <{{#82 | MISSING}|Equipment|Coherent Light Source}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 34 [ label = <{{#81 | MISSING}|Process|Interferometry}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 35 [ label = <{{#80 | MISSING}|Process|Plasma Cleaning}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 36 [ label = <{{#79 | MISSING}|Equipment|Sputter Coater}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 37 [ label = <{{#78 | ASSIGNED}|Process|Optical Die Imaging}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 38 [ label = <{{#77 | MISSING}|Process|Imaging of Human Blood Cells}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 39 [ label = <{{#76 | COMPLETED}|Process|Video Streaming}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 40 [ label = <{{#75 | COMPLETED}|Equipment|Microphone(s)}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 41 [ label = <{{#74 | COMPLETED}|Equipment|Camcorder}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 42 [ label = <{{#73 | COMPLETED}|Development|Main Camera Rig}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 43 [ label = <{{#72 | MISSING}|Process|STM Non-metallic imaging}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 44 [ label = <{{#71 | MISSING}|Research|STM Metal Pattern Deposition}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 45 [ label = <{{#70 | MISSING}|Research|STM Lithography}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 46 [ label = <{{#69 | MISSING}|Research|STM Surface Topography Manipulation}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 47 [ label = <{{#68 | ASSIGNED}|Equipment|Heating Stirrer}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 48 [ label = <{{#67 | ASSIGNED}|Equipment|Chemical Storage/Containment}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 49 [ label = <{{#66 | COMPLETED}|Process|Video Recording}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 50 [ label = <{{#65 | MISSING}|Process|Gas chromatography}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 51 [ label = <{{#64 | COMPLETED}|Equipment|FFF 3D-Printer}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 52 [ label = <{{#63 | ASSIGNED}|Equipment|OpenFlexure Delta Stage}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 53 [ label = <{{#62 | MISSING}|Process|Lithography}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 54 [ label = <{{#61 | MISSING}|Process|Zeloof Z1 process}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 55 [ label = <{{#60 | MISSING}|Process|Thermal diffusion doping}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 56 [ label = <{{#59 | MISSING}|Process|H₃PO₄/HNO₃/AcOH Etching of Aluminum}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 57 [ label = <{{#58 | MISSING}|Process|Pattern Al}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 58 [ label = <{{#57 | MISSING}|Process|Metal thin film deposition}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 59 [ label = <{{#56 | MISSING}|Process|PVD: Thermal Evaporation}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 60 [ label = <{{#55 | ASSIGNED}|Equipment|Tube Furnace}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 61 [ label = <{{#54 | MISSING}|Process|Si Thermal Oxidation}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 62 [ label = <{{#53 | MISSING}|Process|Pattern SiO₂}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 63 [ label = <{{#52 | MISSING}|Development|Maskless photolithography stepper}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 64 [ label = <{{#51 | MISSING}|Process|Maskless Photolithography}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 65 [ label = <{{#50 | MISSING}|Process|E-beam Lithography}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 66 [ label = <{{#49 | ASSIGNED}|Development|Spin Coater}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 67 [ label = <{{#48 | MISSING}|Process|STM Imaging Individual Atoms}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 68 [ label = <{{#47 | MISSING}|Process|STM Imaging@1nm}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 69 [ label = <{{#46 | MISSING}|Process|STM Imaging@10nm}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 70 [ label = <{{#45 | ASSIGNED}|Process|STM Imaging@100nm}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 71 [ label = <{{#44 | ASSIGNED}|Process|STM Imaging@1µm}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 72 [ label = <{{#43 | ASSIGNED}|Development|Scanning Tunneling Microscope (STM)}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 73 [ label = <{{#42 | COMPLETED}|Development|STM Vibration Isolation}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 74 [ label = <{{#41 | COMPLETED}|Process|Primitive Die Imaging}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 75 [ label = <{{#39 | ASSIGNED}|Development|OBI Lite}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 76 [ label = <{{#38 | MISSING}|Process|SEM Non-Metallic Imaging}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 77 [ label = <{{#37 | MISSING}|Process|PVD: Sputtering}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 78 [ label = <{{#36 | MISSING}|Process|Primitive Chip Imaging}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 79 [ label = <{{#35 | MISSING}|Equipment|SEM Beam Blanker}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 80 [ label = <{{#34 | COMPLETED}|Equipment|Open Beam Interface}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 81 [ label = <{{#33 | COMPLETED}|Equipment|SEM Vibration Isolation}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 82 [ label = <{{#32 | MISSING}|Process|SEM Imaging@10nm}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 83 [ label = <{{#31 | COMPLETED}|Process|SEM Imaging@100nm}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 84 [ label = <{{#30 | COMPLETED}|Process|SEM Imaging@1µm}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 85 [ label = <{{#29 | COMPLETED}|Process|SEM Imaging@10µm}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 86 [ label = <{{#28 | COMPLETED}|Equipment|Scanning Electron Microscope (SEM)}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 87 [ label = <{{#27 | ASSIGNED}|Equipment|Technical Ventilation}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 88 [ label = <{{#26 | COMPLETED}|Equipment|Water Cooling}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 89 [ label = <{{#25 | MISSING}|Equipment|Plasma Etcher}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 90 [ label = <{{#24 | COMPLETED}|Equipment|~3ph Power (below 10kW)}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"] + 91 [ label = <{{#23 | ASSIGNED}|Equipment|Fume Hood}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 92 [ label = <{{#21 | MISSING}|Process|Reactive Ion Etching}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 93 [ label = <{{#20 | MISSING}|Process|Body Bias Injection}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 94 [ label = <{{#19 | MISSING}|Process|IC Fault Injection}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 95 [ label = <{{#18 | MISSING}|Research|SEM Fault Injection}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 96 [ label = <{{#17 | MISSING}|Process|SEM Nanoprobing/Live Analysis}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 97 [ label = <{{#16 | MISSING}|Process|Laser Fault Injection}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 98 [ label = <{{#15 | MISSING}|Process|Microprobing}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 99 [ label = <{{#14 | ASSIGNED}|Equipment|Optical Microscope}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 100 [ label = <{{#13 | MISSING}|Process|Full Chip Reverse Engineering}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 101 [ label = <{{#12 | MISSING}|Process|DASH Stain}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 102 [ label = <{{#11 | MISSING}|Process|Simple Chip Imaging}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 103 [ label = <{{#10 | ASSIGNED}|Equipment|SEM: Motorized Stage}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 104 [ label = <{{#9 | ASSIGNED}|Development|Optical Microscope: Motorized Stage}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 105 [ label = <{{#8 | MISSING}|Process|Plasma Etching}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 106 [ label = <{{#7 | MISSING}|Process|Laser Cutting}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 107 [ label = <{{#6 | MISSING}|Process|HNO₃/H₂SO₄ Decapping}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 108 [ label = <{{#5 | MISSING}|Process|BOE Etching of SiO₂}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 109 [ label = <{{#4 | ASSIGNED}|Equipment|Wet Lab (Chemistry)}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"] + 110 [ label = <{{#3 | MISSING}|Equipment|Lapping Machine}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#ffddc1", style = "filled"] + 111 [ label = <{{#2 | MISSING}|Process|Chip Delayering}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 112 [ label = <{{#1 | MISSING}|Process|Chip Decapping}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"] + 0 -> 10 [ ] + 0 -> 15 [ ] + 0 -> 54 [ ] + 1 -> 21 [ ] + 1 -> 94 [ ] + 1 -> 100 [ ] + 2 -> 39 [ ] + 2 -> 49 [ ] + 3 -> 38 [ ] + 3 -> 43 [ ] + 3 -> 46 [ ] + 3 -> 67 [ ] + 3 -> 76 [ ] + 3 -> 82 [ ] + 4 -> 48 [ ] + 9 -> 6 [ ] + 9 -> 8 [ ] + 10 -> 11 [ ] + 10 -> 12 [ ] + 12 -> 16 [ ] + 14 -> 36 [ ] + 15 -> 62 [ ] + 16 -> 13 [ ] + 16 -> 14 [ ] + 16 -> 35 [ ] + 16 -> 56 [ ] + 16 -> 66 [ ] + 16 -> 109 [ ] + 21 -> 26 [ ] + 25 -> 17 [ ] + 25 -> 18 [ ] + 25 -> 19 [ ] + 25 -> 20 [ ] + 25 -> 22 [ ] + 25 -> 23 [ ] + 25 -> 24 [ ] + 26 -> 110 [ ] + 27 -> 7 [ ] + 28 -> 30 [ ] + 29 -> 51 [ ] + 32 -> 28 [ ] + 33 -> 28 [ ] + 33 -> 31 [ ] + 33 -> 32 [ ] + 34 -> 29 [ ] + 34 -> 33 [ ] + 35 -> 89 [ ] + 37 -> 104 [ ] + 38 -> 76 [ ] + 38 -> 83 [ ] + 38 -> 109 [ ] + 39 -> 42 [ ] + 42 -> 40 [ ] + 42 -> 41 [ ] + 43 -> 58 [ ] + 43 -> 72 [ ] + 44 -> 72 [ ] + 45 -> 66 [ ] + 45 -> 72 [ ] + 46 -> 68 [ ] + 48 -> 87 [ ] + 49 -> 5 [ ] + 49 -> 42 [ ] + 50 -> 109 [ ] + 52 -> 51 [ ] + 53 -> 45 [ ] + 53 -> 64 [ ] + 53 -> 65 [ ] + 54 -> 55 [ ] + 54 -> 57 [ ] + 54 -> 62 [ ] + 54 -> 98 [ ] + 55 -> 60 [ ] + 55 -> 66 [ ] + 56 -> 109 [ ] + 57 -> 44 [ ] + 57 -> 53 [ ] + 57 -> 56 [ ] + 57 -> 58 [ ] + 58 -> 59 [ ] + 58 -> 77 [ ] + 61 -> 60 [ ] + 62 -> 53 [ ] + 62 -> 61 [ ] + 62 -> 92 [ ] + 62 -> 108 [ ] + 64 -> 63 [ ] + 64 -> 66 [ ] + 65 -> 66 [ ] + 65 -> 75 [ ] + 65 -> 79 [ ] + 67 -> 68 [ ] + 68 -> 69 [ ] + 69 -> 70 [ ] + 70 -> 71 [ ] + 71 -> 72 [ ] + 72 -> 27 [ ] + 72 -> 34 [ ] + 72 -> 73 [ ] + 74 -> 85 [ ] + 75 -> 80 [ ] + 76 -> 58 [ ] + 76 -> 86 [ ] + 77 -> 36 [ ] + 78 -> 74 [ ] + 78 -> 112 [ ] + 79 -> 86 [ ] + 80 -> 86 [ ] + 81 -> 86 [ ] + 82 -> 81 [ ] + 82 -> 83 [ ] + 83 -> 84 [ ] + 84 -> 85 [ ] + 85 -> 86 [ ] + 86 -> 88 [ ] + 89 -> 87 [ ] + 89 -> 90 [ ] + 91 -> 87 [ ] + 92 -> 89 [ ] + 93 -> 112 [ ] + 94 -> 93 [ ] + 94 -> 95 [ ] + 94 -> 97 [ ] + 94 -> 98 [ ] + 95 -> 79 [ ] + 95 -> 96 [ ] + 96 -> 103 [ ] + 96 -> 111 [ ] + 97 -> 104 [ ] + 97 -> 112 [ ] + 98 -> 27 [ ] + 98 -> 104 [ ] + 100 -> 37 [ ] + 100 -> 101 [ ] + 100 -> 102 [ ] + 101 -> 109 [ ] + 102 -> 78 [ ] + 102 -> 83 [ ] + 102 -> 103 [ ] + 102 -> 111 [ ] + 103 -> 86 [ ] + 104 -> 52 [ ] + 104 -> 99 [ ] + 105 -> 89 [ ] + 107 -> 109 [ ] + 108 -> 109 [ ] + 109 -> 47 [ ] + 109 -> 48 [ ] + 109 -> 87 [ ] + 109 -> 91 [ ] + 111 -> 92 [ ] + 111 -> 106 [ ] + 111 -> 108 [ ] + 111 -> 110 [ ] + 111 -> 112 [ ] + 112 -> 105 [ ] + 112 -> 106 [ ] + 112 -> 107 [ ] + 112 -> 110 [ ] + +} diff --git a/techtree.svg b/techtree.svg new file mode 100644 index 0000000..195092c --- /dev/null +++ b/techtree.svg @@ -0,0 +1,2322 @@ + + + + + + + + + +0 + +#115 + +MISSING + +Process + +Semiconductor Manufacturing + + + +10 + +#105 + +ASSIGNED + +Process + +Create a Backlit LCD Device + + + +0->10 + + + + + +15 + +#100 + +MISSING + +Process + +Etch fractals on wafers + + + +0->15 + + + + + +54 + +#61 + +MISSING + +Process + +Zeloof Z1 process + + + +0->54 + + + + + +1 + +#114 + +MISSING + +Process + +Semiconductor & Electronics Analysis + + + +21 + +#94 + +ASSIGNED + +Process + +PCB Reverse Engineering + + + +1->21 + + + + + +94 + +#19 + +MISSING + +Process + +IC Fault Injection + + + +1->94 + + + + + +100 + +#13 + +MISSING + +Process + +Full Chip Reverse Engineering + + + +1->100 + + + + + +2 + +#113 + +MISSING + +Process + +Science Communication + + + +39 + +#76 + +COMPLETED + +Process + +Video Streaming + + + +2->39 + + + + + +49 + +#66 + +COMPLETED + +Process + +Video Recording + + + +2->49 + + + + + +3 + +#112 + +MISSING + +Process + +Nanoscale Imaging + + + +38 + +#77 + +MISSING + +Process + +Imaging of Human Blood Cells + + + +3->38 + + + + + +43 + +#72 + +MISSING + +Process + +STM Non-metallic imaging + + + +3->43 + + + + + +46 + +#69 + +MISSING + +Research + +STM Surface Topography Manipulation + + + +3->46 + + + + + +67 + +#48 + +MISSING + +Process + +STM Imaging Individual Atoms + + + +3->67 + + + + + +76 + +#38 + +MISSING + +Process + +SEM Non-Metallic Imaging + + + +3->76 + + + + + +82 + +#32 + +MISSING + +Process + +SEM Imaging@10nm + + + +3->82 + + + + + +4 + +#111 + +MISSING + +Process + +LIN supply + + + +48 + +#67 + +ASSIGNED + +Equipment + +Chemical Storage/Containment + + + +4->48 + + + + + +9 + +#106 + +MISSING + +Process + +Electronics Distribution + + + +6 + +#109 + +MISSING + +Equipment + +Environmental Chamber + + + +9->6 + + + + + +8 + +#107 + +MISSING + +Process + +EMI Precompliance Testing + + + +9->8 + + + + + +25 + +#90 + +ASSIGNED + +Process + +PCB Repair & Rework + + + +17 + +#98 + +ASSIGNED + +Development + +Gridfinity-compatible JEDEC IC holders + + + +25->17 + + + + + +18 + +#97 + +COMPLETED + +Equipment + +PCB Rework Microscope + + + +25->18 + + + + + +19 + +#96 + +COMPLETED + +Equipment + +Soldering Station + + + +25->19 + + + + + +20 + +#95 + +MISSING + +Equipment + +Thermal Camera + + + +25->20 + + + + + +22 + +#93 + +COMPLETED + +Equipment + +HEPA Fume Extractor + + + +25->22 + + + + + +23 + +#92 + +COMPLETED + +Equipment + +Miscellaneous Tools for BGA Rework + + + +25->23 + + + + + +24 + +#91 + +COMPLETED + +Equipment + +Hot Air Rework Station + + + +25->24 + + + + + +50 + +#65 + +MISSING + +Process + +Gas chromatography + + + +109 + +#4 + +ASSIGNED + +Equipment + +Wet Lab (Chemistry) + + + +50->109 + + + + + +5 + +#110 + +ASSIGNED + +Equipment + +Local NAS/server + + + +7 + +#108 + +MISSING + +Development + +Piezo Driver + + + +11 + +#104 + +ASSIGNED + +Process + +Electroluminescence Foil + + + +10->11 + + + + + +12 + +#103 + +MISSING + +Process + +Create a Matrix LCD Device + + + +10->12 + + + + + +16 + +#99 + +ASSIGNED + +Process + +Create an LCD Device + + + +12->16 + + + + + +13 + +#102 + +ASSIGNED + +Research + +Acquire Liquid Crystal Fluid + + + +14 + +#101 + +MISSING + +Process + +Chemical Vapor Deposition + + + +36 + +#79 + +MISSING + +Equipment + +Sputter Coater + + + +14->36 + + + + + +62 + +#53 + +MISSING + +Process + +Pattern SiO₂ + + + +15->62 + + + + + +16->13 + + + + + +16->14 + + + + + +35 + +#80 + +MISSING + +Process + +Plasma Cleaning + + + +16->35 + + + + + +56 + +#59 + +MISSING + +Process + +H₃PO₄/HNO₃/AcOH Etching of Aluminum + + + +16->56 + + + + + +66 + +#49 + +ASSIGNED + +Development + +Spin Coater + + + +16->66 + + + + + +16->109 + + + + + +26 + +#89 + +ASSIGNED + +Process + +PCB Delayering + + + +21->26 + + + + + +110 + +#3 + +MISSING + +Equipment + +Lapping Machine + + + +26->110 + + + + + +27 + +#88 + +MISSING + +Development + +Piezo Slider Actuators + + + +27->7 + + + + + +28 + +#87 + +MISSING + +Equipment + +Diode Laser + + + +30 + +#85 + +ASSIGNED + +Development + +Laser Diode Driver/Controller + + + +28->30 + + + + + +29 + +#86 + +ASSIGNED + +Development + +Optomechanical System + + + +51 + +#64 + +COMPLETED + +Equipment + +FFF 3D-Printer + + + +29->51 + + + + + +31 + +#84 + +MISSING + +Equipment + +Stabilized Helium-Neon Laser + + + +32 + +#83 + +MISSING + +Equipment + +External Cavity Diode Laser (ECDL) + + + +32->28 + + + + + +33 + +#82 + +MISSING + +Equipment + +Coherent Light Source + + + +33->28 + + + + + +33->31 + + + + + +33->32 + + + + + +34 + +#81 + +MISSING + +Process + +Interferometry + + + +34->29 + + + + + +34->33 + + + + + +89 + +#25 + +MISSING + +Equipment + +Plasma Etcher + + + +35->89 + + + + + +37 + +#78 + +ASSIGNED + +Process + +Optical Die Imaging + + + +104 + +#9 + +ASSIGNED + +Development + +Optical Microscope: Motorized Stage + + + +37->104 + + + + + +38->76 + + + + + +83 + +#31 + +COMPLETED + +Process + +SEM Imaging@100nm + + + +38->83 + + + + + +38->109 + + + + + +42 + +#73 + +COMPLETED + +Development + +Main Camera Rig + + + +39->42 + + + + + +40 + +#75 + +COMPLETED + +Equipment + +Microphone(s) + + + +41 + +#74 + +COMPLETED + +Equipment + +Camcorder + + + +42->40 + + + + + +42->41 + + + + + +58 + +#57 + +MISSING + +Process + +Metal thin film deposition + + + +43->58 + + + + + +72 + +#43 + +ASSIGNED + +Development + +Scanning Tunneling Microscope (STM) + + + +43->72 + + + + + +44 + +#71 + +MISSING + +Research + +STM Metal Pattern Deposition + + + +44->72 + + + + + +45 + +#70 + +MISSING + +Research + +STM Lithography + + + +45->66 + + + + + +45->72 + + + + + +68 + +#47 + +MISSING + +Process + +STM Imaging@1nm + + + +46->68 + + + + + +47 + +#68 + +ASSIGNED + +Equipment + +Heating Stirrer + + + +87 + +#27 + +ASSIGNED + +Equipment + +Technical Ventilation + + + +48->87 + + + + + +49->5 + + + + + +49->42 + + + + + +52 + +#63 + +ASSIGNED + +Equipment + +OpenFlexure Delta Stage + + + +52->51 + + + + + +53 + +#62 + +MISSING + +Process + +Lithography + + + +53->45 + + + + + +64 + +#51 + +MISSING + +Process + +Maskless Photolithography + + + +53->64 + + + + + +65 + +#50 + +MISSING + +Process + +E-beam Lithography + + + +53->65 + + + + + +55 + +#60 + +MISSING + +Process + +Thermal diffusion doping + + + +54->55 + + + + + +57 + +#58 + +MISSING + +Process + +Pattern Al + + + +54->57 + + + + + +54->62 + + + + + +98 + +#15 + +MISSING + +Process + +Microprobing + + + +54->98 + + + + + +60 + +#55 + +ASSIGNED + +Equipment + +Tube Furnace + + + +55->60 + + + + + +55->66 + + + + + +56->109 + + + + + +57->44 + + + + + +57->53 + + + + + +57->56 + + + + + +57->58 + + + + + +59 + +#56 + +MISSING + +Process + +PVD: Thermal Evaporation + + + +58->59 + + + + + +77 + +#37 + +MISSING + +Process + +PVD: Sputtering + + + +58->77 + + + + + +61 + +#54 + +MISSING + +Process + +Si Thermal Oxidation + + + +61->60 + + + + + +62->53 + + + + + +62->61 + + + + + +92 + +#21 + +MISSING + +Process + +Reactive Ion Etching + + + +62->92 + + + + + +108 + +#5 + +MISSING + +Process + +BOE Etching of SiO₂ + + + +62->108 + + + + + +63 + +#52 + +MISSING + +Development + +Maskless photolithography stepper + + + +64->63 + + + + + +64->66 + + + + + +65->66 + + + + + +75 + +#39 + +ASSIGNED + +Development + +OBI Lite + + + +65->75 + + + + + +79 + +#35 + +MISSING + +Equipment + +SEM Beam Blanker + + + +65->79 + + + + + +67->68 + + + + + +69 + +#46 + +MISSING + +Process + +STM Imaging@10nm + + + +68->69 + + + + + +70 + +#45 + +ASSIGNED + +Process + +STM Imaging@100nm + + + +69->70 + + + + + +71 + +#44 + +ASSIGNED + +Process + +STM Imaging@1µm + + + +70->71 + + + + + +71->72 + + + + + +72->27 + + + + + +72->34 + + + + + +73 + +#42 + +COMPLETED + +Development + +STM Vibration Isolation + + + +72->73 + + + + + +74 + +#41 + +COMPLETED + +Process + +Primitive Die Imaging + + + +85 + +#29 + +COMPLETED + +Process + +SEM Imaging@10µm + + + +74->85 + + + + + +80 + +#34 + +COMPLETED + +Equipment + +Open Beam Interface + + + +75->80 + + + + + +76->58 + + + + + +86 + +#28 + +COMPLETED + +Equipment + +Scanning Electron Microscope (SEM) + + + +76->86 + + + + + +77->36 + + + + + +78 + +#36 + +MISSING + +Process + +Primitive Chip Imaging + + + +78->74 + + + + + +112 + +#1 + +MISSING + +Process + +Chip Decapping + + + +78->112 + + + + + +79->86 + + + + + +80->86 + + + + + +81 + +#33 + +COMPLETED + +Equipment + +SEM Vibration Isolation + + + +81->86 + + + + + +82->81 + + + + + +82->83 + + + + + +84 + +#30 + +COMPLETED + +Process + +SEM Imaging@1µm + + + +83->84 + + + + + +84->85 + + + + + +85->86 + + + + + +88 + +#26 + +COMPLETED + +Equipment + +Water Cooling + + + +86->88 + + + + + +89->87 + + + + + +90 + +#24 + +COMPLETED + +Equipment + +~3ph Power (below 10kW) + + + +89->90 + + + + + +91 + +#23 + +ASSIGNED + +Equipment + +Fume Hood + + + +91->87 + + + + + +92->89 + + + + + +93 + +#20 + +MISSING + +Process + +Body Bias Injection + + + +93->112 + + + + + +94->93 + + + + + +95 + +#18 + +MISSING + +Research + +SEM Fault Injection + + + +94->95 + + + + + +97 + +#16 + +MISSING + +Process + +Laser Fault Injection + + + +94->97 + + + + + +94->98 + + + + + +95->79 + + + + + +96 + +#17 + +MISSING + +Process + +SEM Nanoprobing/Live Analysis + + + +95->96 + + + + + +103 + +#10 + +ASSIGNED + +Equipment + +SEM: Motorized Stage + + + +96->103 + + + + + +111 + +#2 + +MISSING + +Process + +Chip Delayering + + + +96->111 + + + + + +97->104 + + + + + +97->112 + + + + + +98->27 + + + + + +98->104 + + + + + +99 + +#14 + +ASSIGNED + +Equipment + +Optical Microscope + + + +100->37 + + + + + +101 + +#12 + +MISSING + +Process + +DASH Stain + + + +100->101 + + + + + +102 + +#11 + +MISSING + +Process + +Simple Chip Imaging + + + +100->102 + + + + + +101->109 + + + + + +102->78 + + + + + +102->83 + + + + + +102->103 + + + + + +102->111 + + + + + +103->86 + + + + + +104->52 + + + + + +104->99 + + + + + +105 + +#8 + +MISSING + +Process + +Plasma Etching + + + +105->89 + + + + + +106 + +#7 + +MISSING + +Process + +Laser Cutting + + + +107 + +#6 + +MISSING + +Process + +HNO₃/H₂SO₄ Decapping + + + +107->109 + + + + + +108->109 + + + + + +109->47 + + + + + +109->48 + + + + + +109->87 + + + + + +109->91 + + + + + +111->92 + + + + + +111->106 + + + + + +111->108 + + + + + +111->110 + + + + + +111->112 + + + + + +112->105 + + + + + +112->106 + + + + + +112->107 + + + + + +112->110 + + + + +