Compare commits
No commits in common. "main" and "render" have entirely different histories.
|
@ -1,14 +0,0 @@
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, reopened, closed, labeled, 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
|
|
|
@ -1,17 +0,0 @@
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
- 'poc'
|
|
||||||
|
|
||||||
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
|
|
140
README.md
140
README.md
|
@ -1,140 +0,0 @@
|
||||||
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 tree's elements. For a better overview, check each issue for a
|
|
||||||
filtered subtree that contains just the elements related to it.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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, color:#000;
|
|
||||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
|
||||||
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\n<i>Process</i>\n<b>Technology A</b>"]
|
|
||||||
1:::dep_completed
|
|
||||||
1["#2 | COMPLETED\n<i>Process</i>\n<b>Technology B</b>"]
|
|
||||||
2:::eoi
|
|
||||||
2["#3 | MISSING\n<i>Process</i>\n<b>Element C</b>"]
|
|
||||||
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, color:#000;
|
|
||||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
|
||||||
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\n<i>Process</i>\n<b>Processing X using machine A</b>"]
|
|
||||||
1:::dep_completed
|
|
||||||
1["#1 | COMPLETED\n<i>Equipment</i>\n<b>Generic Machine A</b>"]
|
|
||||||
2:::eoi
|
|
||||||
2["#3 | MISSING\n<i>Process</i>\n<b>Element C</b>"]
|
|
||||||
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 is labelled `Completed`. 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.
|
|
||||||
|
|
||||||
- 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 closing them.
|
|
2
techtree-manager/.gitignore
vendored
2
techtree-manager/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
/target/
|
|
||||||
/render-git/
|
|
1987
techtree-manager/Cargo.lock
generated
1987
techtree-manager/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,22 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "techtree-manager"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
authors = ["rahix <rahix@rahix.de>"]
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[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"
|
|
||||||
time = "0.3.41"
|
|
||||||
tokio = { version = "1.45.0", features = ["full"] }
|
|
||||||
url = "2.5.4"
|
|
|
@ -1,201 +0,0 @@
|
||||||
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.
|
|
|
@ -1,23 +0,0 @@
|
||||||
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.
|
|
|
@ -1,21 +0,0 @@
|
||||||
# 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
|
|
||||||
pkgs.graphviz
|
|
||||||
pkgs.git
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
export OPENSSL_DIR="${pkgs.openssl.dev}"
|
|
||||||
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
use anyhow::Context as _;
|
|
||||||
|
|
||||||
/// Read all issues to generate the full techtree
|
|
||||||
pub async fn collect_tree(ctx: &crate::Context) -> anyhow::Result<crate::tree::Tree> {
|
|
||||||
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();
|
|
||||||
|
|
||||||
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("<unknown>".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 = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_list_issue_dependencies(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
// Why the hell is the issue number a string here?
|
|
||||||
&issue.to_string(),
|
|
||||||
forgejo_api::structs::IssueListIssueDependenciesQuery {
|
|
||||||
limit: Some(10000),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.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")?;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_from_issue(issue: &forgejo_api::structs::Issue) -> anyhow::Result<crate::tree::Element> {
|
|
||||||
let issue_number = issue.number.context("Missing issue number")?;
|
|
||||||
let description = issue
|
|
||||||
.title
|
|
||||||
.as_deref()
|
|
||||||
.context("Issue is missing a title")?
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let labels = issue
|
|
||||||
.labels
|
|
||||||
.as_ref()
|
|
||||||
.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/"))
|
|
||||||
.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()
|
|
||||||
.unwrap()
|
|
||||||
.strip_prefix("Type/")
|
|
||||||
.unwrap()
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let has_completed_label = labels
|
|
||||||
.iter()
|
|
||||||
.any(|l| l.name.as_deref() == Some("Completed"));
|
|
||||||
|
|
||||||
let status = match issue.state.context("Missing issue state")? {
|
|
||||||
forgejo_api::structs::StateType::Open => {
|
|
||||||
if has_completed_label {
|
|
||||||
crate::tree::ElementStatus::Completed
|
|
||||||
} else 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 => anyhow::bail!("Ignoring closed issue!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(crate::tree::Element {
|
|
||||||
issue_number,
|
|
||||||
description,
|
|
||||||
ty,
|
|
||||||
status,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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<IssueEventMeta> {
|
|
||||||
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".to_owned(),
|
|
||||||
owner: "fafo".to_owned(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
use anyhow::Context as _;
|
|
||||||
|
|
||||||
pub type CommentId = u64;
|
|
||||||
|
|
||||||
pub struct BotCommentInfo {
|
|
||||||
pub body: String,
|
|
||||||
pub id: CommentId,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn make_bot_comment(
|
|
||||||
ctx: &crate::Context,
|
|
||||||
issue_number: u64,
|
|
||||||
) -> anyhow::Result<BotCommentInfo> {
|
|
||||||
let initial_message =
|
|
||||||
"_Please be patient, this issue is currently being integrated into the techtree..._";
|
|
||||||
|
|
||||||
let res = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_create_comment(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
issue_number,
|
|
||||||
forgejo_api::structs::CreateIssueCommentOption {
|
|
||||||
body: initial_message.to_owned(),
|
|
||||||
updated_at: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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<Option<BotCommentInfo>> {
|
|
||||||
let mut comments = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_get_comments(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
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(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
new_body: String,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
ctx.forgejo
|
|
||||||
.issue_edit_comment(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
id,
|
|
||||||
forgejo_api::structs::EditIssueCommentOption {
|
|
||||||
body: new_body,
|
|
||||||
updated_at: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("Failed to update comment body")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_bot_comment_from_subtree(
|
|
||||||
ctx: &crate::Context,
|
|
||||||
id: CommentId,
|
|
||||||
subtree: &crate::tree::Subtree<'_>,
|
|
||||||
hash: &str,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), false);
|
|
||||||
|
|
||||||
// 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..._
|
|
||||||
|
|
||||||
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
|
|
||||||
timestamp = ctx.timestamp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
r##"## Partial Techtree
|
|
||||||
```mermaid
|
|
||||||
{mermaid}
|
|
||||||
```
|
|
||||||
|
|
||||||
<small>Digest: {hash}; Last Updated: {timestamp}</small>"##,
|
|
||||||
timestamp = ctx.timestamp,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
update_bot_comment(&ctx, id, full_text).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_stale_label(ctx: &crate::Context, issue_number: u64) -> anyhow::Result<()> {
|
|
||||||
let labels = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_get_labels(&ctx.owner, &ctx.repo, issue_number)
|
|
||||||
.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}...");
|
|
||||||
|
|
||||||
let res = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_remove_label(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
issue_number,
|
|
||||||
stale_label_id,
|
|
||||||
forgejo_api::structs::DeleteLabelsOption {
|
|
||||||
updated_at: Some(time::OffsetDateTime::now_utc()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.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(())
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
|
||||||
use forgejo_api::Forgejo;
|
|
||||||
|
|
||||||
mod collect;
|
|
||||||
mod event_meta;
|
|
||||||
mod issue;
|
|
||||||
mod render;
|
|
||||||
mod tree;
|
|
||||||
mod wiki;
|
|
||||||
|
|
||||||
pub struct Context {
|
|
||||||
/// API Accessor object
|
|
||||||
pub forgejo: forgejo_api::Forgejo,
|
|
||||||
/// Repository Owner
|
|
||||||
pub owner: String,
|
|
||||||
/// Repository Name
|
|
||||||
pub repo: String,
|
|
||||||
/// URL of the repository page
|
|
||||||
pub repo_url: url::Url,
|
|
||||||
/// URL of the repository with authentication information attached
|
|
||||||
pub repo_auth_url: 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!");
|
|
||||||
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").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 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 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();
|
|
||||||
|
|
||||||
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,
|
|
||||||
owner: meta.issue.repository.owner.clone(),
|
|
||||||
repo: meta.issue.repository.name.clone(),
|
|
||||||
repo_url,
|
|
||||||
repo_auth_url,
|
|
||||||
timestamp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if meta.action == event_meta::IssueAction::Opened {
|
|
||||||
log::debug!("Running for a newly opened issue. Taking care of the comment first...");
|
|
||||||
match issue::find_or_make_bot_comment(&ctx, meta.issue.number).await {
|
|
||||||
Ok((_comment, is_new)) => {
|
|
||||||
if is_new {
|
|
||||||
log::info!(
|
|
||||||
"Waiting for a minute, as the issue is brand new. We will likely catch some more updates this way!"
|
|
||||||
);
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
|
||||||
"Error while commenting on new issue #{}, continuing anyway... Error: {err:?}",
|
|
||||||
meta.issue.number
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Collecting tree from issue metadata...");
|
|
||||||
let tree = collect::collect_tree(&ctx)
|
|
||||||
.await
|
|
||||||
.context("Failed to collect the techtree from issue metadata")?;
|
|
||||||
|
|
||||||
log::info!("Rendering and publishing techtree to git repository...");
|
|
||||||
let rendered_tree = render::render(&ctx, &tree)
|
|
||||||
.await
|
|
||||||
.context("Failed to render the techtree")?;
|
|
||||||
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 {
|
|
||||||
log::info!("Updating the wiki overview...");
|
|
||||||
wiki::update_wiki_from_tree(&ctx, &tree)
|
|
||||||
.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 (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 to remove `Stale` label from issue #{issue}"))?;
|
|
||||||
|
|
||||||
continue 'issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Updating bot comment in issue #{issue} ...");
|
|
||||||
issue::update_bot_comment_from_subtree(&ctx, bot_comment.id, &subtree, &hash)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("Failed to update bot comment in issue #{issue}"))?;
|
|
||||||
|
|
||||||
issue::remove_stale_label(&ctx, issue)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("Failed to remove `Stale` label from issue #{issue}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
|
||||||
run().await
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
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 struct RenderedTree {
|
|
||||||
repo_dir: std::path::PathBuf,
|
|
||||||
dot_file: std::path::PathBuf,
|
|
||||||
svg_file: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render(
|
|
||||||
_ctx: &crate::Context,
|
|
||||||
tree: &crate::tree::Tree,
|
|
||||||
) -> anyhow::Result<RenderedTree> {
|
|
||||||
let repo_dir = std::path::PathBuf::from("render-git");
|
|
||||||
|
|
||||||
if repo_dir.is_dir() {
|
|
||||||
log::info!("Found old {repo_dir:?} repository, removing...");
|
|
||||||
std::fs::remove_dir_all(&repo_dir).context("Failed to remove stale render repository")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::create_dir(&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");
|
|
||||||
|
|
||||||
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)
|
|
||||||
.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(&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(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(&rendered.repo_dir)
|
|
||||||
.arg("commit")
|
|
||||||
.args(["-m", &format!("Updated techtree at {}", ctx.timestamp)])
|
|
||||||
.success()
|
|
||||||
.context("Failed to add generated graph files to git index")?;
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&rendered.repo_dir)
|
|
||||||
.arg("push")
|
|
||||||
.arg("--force")
|
|
||||||
.arg("--quiet")
|
|
||||||
.arg(ctx.repo_auth_url.to_string())
|
|
||||||
.arg("HEAD:refs/heads/render")
|
|
||||||
.status()
|
|
||||||
.context("Failed to push rendered graph to forgejo repository")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,353 +0,0 @@
|
||||||
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: String,
|
|
||||||
/// Completion status of this element.
|
|
||||||
pub status: ElementStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element {
|
|
||||||
fn to_dot_node_attributes(&self, role: Option<SubtreeElementRole>) -> String {
|
|
||||||
let Element {
|
|
||||||
issue_number,
|
|
||||||
description,
|
|
||||||
ty,
|
|
||||||
status,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let mut attributes = Vec::new();
|
|
||||||
|
|
||||||
attributes.push(format!(
|
|
||||||
r##"label = <{{{{#{issue_number} | {status}}}|<I>{ty}</I>|<B>{description}</B>}}>"##
|
|
||||||
));
|
|
||||||
attributes.push(r#"shape = "record""#.to_owned());
|
|
||||||
|
|
||||||
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(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_mermaid_node(
|
|
||||||
&self,
|
|
||||||
index: ElementIndex,
|
|
||||||
role: Option<SubtreeElementRole>,
|
|
||||||
repo_url: &str,
|
|
||||||
simple: bool,
|
|
||||||
) -> String {
|
|
||||||
let Element {
|
|
||||||
issue_number,
|
|
||||||
description,
|
|
||||||
ty,
|
|
||||||
status,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let label = if simple {
|
|
||||||
format!(r##"#{issue_number} | {status}\n<i>{ty}</i>\n<b>{description}</b>"##)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
r##"<a href='{repo_url}/issues/{issue_number}' target='_blank'>#{issue_number}</a> | {status}\n<i>{ty}</i>\n<b>{description}</b>"##
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
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, color:#000;
|
|
||||||
classDef dependant fill:#fff, stroke:#888, color:#888;
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Element, ()>,
|
|
||||||
issue_map: BTreeMap<u64, ElementIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ElementIndex> {
|
|
||||||
self.issue_map.get(&issue_number).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subtree_for_issue<'a>(&'a self, issue_number: u64) -> Option<Subtree<'a>> {
|
|
||||||
Some(Subtree::new_for_element(
|
|
||||||
self,
|
|
||||||
self.find_element_by_issue_number(issue_number)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter_issues<'a>(&'a self) -> impl Iterator<Item = u64> + '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),
|
|
||||||
petgraph::dot::Config::GraphContentOnly,
|
|
||||||
],
|
|
||||||
&|_g, _edge_id| "".to_string(),
|
|
||||||
&|_g, (_, element)| element.to_dot_node_attributes(None),
|
|
||||||
);
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"digraph {{
|
|
||||||
ranksep=1.2
|
|
||||||
{:?}
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
dot
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, simple));
|
|
||||||
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<SubtreeElement<'a>, ()>,
|
|
||||||
index_map: BTreeMap<ElementIndex, ElementIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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,
|
|
||||||
simple,
|
|
||||||
));
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
use anyhow::Context as _;
|
|
||||||
use base64::prelude::*;
|
|
||||||
|
|
||||||
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(), false);
|
|
||||||
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
|
|
||||||
.repo_edit_wiki_page(
|
|
||||||
&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 {}", ctx.timestamp)),
|
|
||||||
title: Some("Home".to_owned()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("Failed editing the wiki page");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
184
techtree.dot
Normal file
184
techtree.dot
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
digraph {
|
||||||
|
ranksep=1.2
|
||||||
|
rankdir="BT"
|
||||||
|
0 [ label = <{{#76 | ASSIGNED}|<I>Process</I>|<B>Video Streaming</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
1 [ label = <{{#75 | ASSIGNED}|<I>Equipment</I>|<B>Microphone(s)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
2 [ label = <{{#74 | ASSIGNED}|<I>Equipment</I>|<B>Camcorder</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
3 [ label = <{{#73 | ASSIGNED}|<I>Development</I>|<B>Main Camera Rig</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
4 [ label = <{{#72 | MISSING}|<I>Process</I>|<B>STM Non-metallic imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
5 [ label = <{{#71 | MISSING}|<I>Research</I>|<B>STM Metal Pattern Deposition</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
6 [ label = <{{#70 | MISSING}|<I>Research</I>|<B>STM Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
7 [ label = <{{#69 | MISSING}|<I>Research</I>|<B>STM Surface Topography Manipulation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
8 [ label = <{{#68 | ASSIGNED}|<I>Equipment</I>|<B>Heating Stirrer</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
9 [ label = <{{#67 | MISSING}|<I>Equipment</I>|<B>Chemical Storage/Containment</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
10 [ label = <{{#66 | ASSIGNED}|<I>Process</I>|<B>Video Recording</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
11 [ label = <{{#65 | MISSING}|<I>Process</I>|<B>Gas chromatography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
12 [ label = <{{#64 | COMPLETED}|<I>Equipment</I>|<B>FFF 3D-Printer</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
13 [ label = <{{#63 | MISSING}|<I>Equipment</I>|<B>OpenFlexure Delta Stage</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
14 [ label = <{{#62 | MISSING}|<I>Process</I>|<B>Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
15 [ label = <{{#61 | MISSING}|<I>Process</I>|<B>Zeloof Z1 process</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
16 [ label = <{{#60 | MISSING}|<I>Process</I>|<B>Thermal diffusion doping</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
17 [ label = <{{#59 | MISSING}|<I>Process</I>|<B>H₃PO₄/HNO₃/AcOH Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
18 [ label = <{{#58 | MISSING}|<I>Process</I>|<B>Pattern Al</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
19 [ label = <{{#57 | MISSING}|<I>Process</I>|<B>Metal thin film deposition</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
20 [ label = <{{#56 | MISSING}|<I>Process</I>|<B>PVD: Thermal Evaporation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
21 [ label = <{{#55 | MISSING}|<I>Equipment</I>|<B>Tube Furnace</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
22 [ label = <{{#54 | MISSING}|<I>Process</I>|<B>Si Thermal Oxidation</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
23 [ label = <{{#53 | MISSING}|<I>Process</I>|<B>Pattern SiO₂</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
24 [ label = <{{#52 | MISSING}|<I>Development</I>|<B>Maskless photolithography stepper</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
25 [ label = <{{#51 | MISSING}|<I>Process</I>|<B>Maskless Photolithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
26 [ label = <{{#50 | MISSING}|<I>Process</I>|<B>E-beam Lithography</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
27 [ label = <{{#49 | MISSING}|<I>Development</I>|<B>Spin Coater</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
28 [ label = <{{#48 | MISSING}|<I>Process</I>|<B>STM Imaging Individual Atoms</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
29 [ label = <{{#47 | MISSING}|<I>Process</I>|<B>STM Imaging@1nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
30 [ label = <{{#46 | MISSING}|<I>Process</I>|<B>STM Imaging@10nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
31 [ label = <{{#45 | MISSING}|<I>Process</I>|<B>STM Imaging@100nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
32 [ label = <{{#44 | MISSING}|<I>Process</I>|<B>STM Imaging@1µm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
33 [ label = <{{#43 | ASSIGNED}|<I>Development</I>|<B>Scanning Tunneling Microscope (STM)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
34 [ label = <{{#42 | ASSIGNED}|<I>Development</I>|<B>STM Vibration Isolation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
35 [ label = <{{#41 | COMPLETED}|<I>Process</I>|<B>Primitive Die Imaging</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
36 [ label = <{{#40 | ASSIGNED}|<I>Equipment</I>|<B>Air Compressor</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
37 [ label = <{{#39 | MISSING}|<I>Development</I>|<B>OBI Lite</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
38 [ label = <{{#38 | MISSING}|<I>Process</I>|<B>SEM Non-Metallic Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
39 [ label = <{{#37 | MISSING}|<I>Process</I>|<B>PVD: Sputtering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
40 [ label = <{{#36 | MISSING}|<I>Process</I>|<B>Primitive Chip Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
41 [ label = <{{#35 | MISSING}|<I>Equipment</I>|<B>SEM Beam Blanker</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
42 [ label = <{{#34 | COMPLETED}|<I>Equipment</I>|<B>Open Beam Interface</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
43 [ label = <{{#33 | ASSIGNED}|<I>Equipment</I>|<B>SEM Vibration Isolation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
44 [ label = <{{#32 | MISSING}|<I>Process</I>|<B>SEM Imaging@10nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
45 [ label = <{{#31 | MISSING}|<I>Process</I>|<B>SEM Imaging@100nm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
46 [ label = <{{#30 | MISSING}|<I>Process</I>|<B>SEM Imaging@1µm</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
47 [ label = <{{#29 | COMPLETED}|<I>Process</I>|<B>SEM Imaging@10µm</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
48 [ label = <{{#28 | COMPLETED}|<I>Equipment</I>|<B>Scanning Electron Microscope (SEM)</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
49 [ label = <{{#27 | ASSIGNED}|<I>Equipment</I>|<B>Technical Ventilation</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
50 [ label = <{{#26 | COMPLETED}|<I>Equipment</I>|<B>Water Cooling</B>}>, shape = "record", color = "#080", fontcolor = "#080", fillcolor = "#afa", style = "filled"]
|
||||||
|
51 [ label = <{{#25 | MISSING}|<I>Equipment</I>|<B>Plasma Etcher</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
52 [ label = <{{#24 | ASSIGNED}|<I>Equipment</I>|<B>~3ph Power (below 10kW)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
53 [ label = <{{#23 | MISSING}|<I>Equipment</I>|<B>Fume Hood</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
54 [ label = <{{#22 | MISSING}|<I>Equipment</I>|<B>Running Water and Sink</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
55 [ label = <{{#21 | MISSING}|<I>Process</I>|<B>Reactive Ion Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
56 [ label = <{{#20 | MISSING}|<I>Process</I>|<B>Body Bias Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
57 [ label = <{{#19 | MISSING}|<I>Process</I>|<B>IC Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
58 [ label = <{{#18 | MISSING}|<I>Research</I>|<B>SEM Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
59 [ label = <{{#17 | MISSING}|<I>Process</I>|<B>SEM Nanoprobing/Live Analysis</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
60 [ label = <{{#16 | MISSING}|<I>Process</I>|<B>Laser Fault Injection</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
61 [ label = <{{#15 | MISSING}|<I>Process</I>|<B>Microprobing</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
62 [ label = <{{#14 | MISSING}|<I>Equipment</I>|<B>Optical Microscope</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
63 [ label = <{{#13 | MISSING}|<I>Process</I>|<B>Full Chip Reverse Engineering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
64 [ label = <{{#12 | MISSING}|<I>Process</I>|<B>DASH Stain</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
65 [ label = <{{#11 | MISSING}|<I>Process</I>|<B>Simple Chip Imaging</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
66 [ label = <{{#10 | ASSIGNED}|<I>Equipment</I>|<B>SEM: Motorized Stage</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
67 [ label = <{{#9 | MISSING}|<I>Equipment</I>|<B>Optical Microscope: Motorized Stage</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
68 [ label = <{{#8 | MISSING}|<I>Process</I>|<B>Plasma Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
69 [ label = <{{#7 | MISSING}|<I>Equipment</I>|<B>Fiber Laser</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
70 [ label = <{{#6 | MISSING}|<I>Process</I>|<B>HNO₃/H₂SO₄ Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
71 [ label = <{{#5 | MISSING}|<I>Process</I>|<B>HNO₃/HF Etching</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
72 [ label = <{{#4 | ASSIGNED}|<I>Equipment</I>|<B>Wet Lab (Chemistry)</B>}>, shape = "record", color = "#a50", fontcolor = "#a50", fillcolor = "#ffa", style = "filled"]
|
||||||
|
73 [ label = <{{#3 | MISSING}|<I>Equipment</I>|<B>Lapping Machine</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
74 [ label = <{{#2 | MISSING}|<I>Process</I>|<B>Chip Delayering</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
75 [ label = <{{#1 | MISSING}|<I>Process</I>|<B>Chip Decapping</B>}>, shape = "record", color = "#800", fontcolor = "#800", fillcolor = "#fcc", style = "filled"]
|
||||||
|
0 -> 3 [ ]
|
||||||
|
3 -> 1 [ ]
|
||||||
|
3 -> 2 [ ]
|
||||||
|
4 -> 19 [ ]
|
||||||
|
4 -> 33 [ ]
|
||||||
|
5 -> 33 [ ]
|
||||||
|
6 -> 27 [ ]
|
||||||
|
6 -> 33 [ ]
|
||||||
|
7 -> 29 [ ]
|
||||||
|
9 -> 49 [ ]
|
||||||
|
10 -> 3 [ ]
|
||||||
|
11 -> 72 [ ]
|
||||||
|
13 -> 12 [ ]
|
||||||
|
14 -> 6 [ ]
|
||||||
|
14 -> 25 [ ]
|
||||||
|
14 -> 26 [ ]
|
||||||
|
15 -> 16 [ ]
|
||||||
|
15 -> 18 [ ]
|
||||||
|
15 -> 23 [ ]
|
||||||
|
15 -> 61 [ ]
|
||||||
|
16 -> 21 [ ]
|
||||||
|
16 -> 72 [ ]
|
||||||
|
17 -> 72 [ ]
|
||||||
|
18 -> 5 [ ]
|
||||||
|
18 -> 14 [ ]
|
||||||
|
18 -> 17 [ ]
|
||||||
|
18 -> 19 [ ]
|
||||||
|
19 -> 20 [ ]
|
||||||
|
19 -> 39 [ ]
|
||||||
|
22 -> 21 [ ]
|
||||||
|
23 -> 14 [ ]
|
||||||
|
23 -> 22 [ ]
|
||||||
|
23 -> 55 [ ]
|
||||||
|
23 -> 71 [ ]
|
||||||
|
25 -> 24 [ ]
|
||||||
|
25 -> 27 [ ]
|
||||||
|
26 -> 27 [ ]
|
||||||
|
26 -> 41 [ ]
|
||||||
|
28 -> 29 [ ]
|
||||||
|
29 -> 30 [ ]
|
||||||
|
30 -> 31 [ ]
|
||||||
|
31 -> 32 [ ]
|
||||||
|
32 -> 33 [ ]
|
||||||
|
33 -> 34 [ ]
|
||||||
|
35 -> 47 [ ]
|
||||||
|
37 -> 42 [ ]
|
||||||
|
38 -> 19 [ ]
|
||||||
|
38 -> 48 [ ]
|
||||||
|
40 -> 35 [ ]
|
||||||
|
40 -> 75 [ ]
|
||||||
|
41 -> 48 [ ]
|
||||||
|
42 -> 48 [ ]
|
||||||
|
43 -> 36 [ ]
|
||||||
|
43 -> 48 [ ]
|
||||||
|
44 -> 45 [ ]
|
||||||
|
45 -> 43 [ ]
|
||||||
|
45 -> 46 [ ]
|
||||||
|
46 -> 47 [ ]
|
||||||
|
47 -> 48 [ ]
|
||||||
|
48 -> 50 [ ]
|
||||||
|
51 -> 49 [ ]
|
||||||
|
51 -> 52 [ ]
|
||||||
|
53 -> 49 [ ]
|
||||||
|
55 -> 51 [ ]
|
||||||
|
56 -> 75 [ ]
|
||||||
|
57 -> 56 [ ]
|
||||||
|
57 -> 58 [ ]
|
||||||
|
57 -> 60 [ ]
|
||||||
|
57 -> 61 [ ]
|
||||||
|
58 -> 41 [ ]
|
||||||
|
58 -> 59 [ ]
|
||||||
|
59 -> 66 [ ]
|
||||||
|
59 -> 74 [ ]
|
||||||
|
60 -> 67 [ ]
|
||||||
|
60 -> 75 [ ]
|
||||||
|
61 -> 67 [ ]
|
||||||
|
63 -> 64 [ ]
|
||||||
|
63 -> 65 [ ]
|
||||||
|
64 -> 72 [ ]
|
||||||
|
65 -> 40 [ ]
|
||||||
|
65 -> 45 [ ]
|
||||||
|
65 -> 66 [ ]
|
||||||
|
65 -> 74 [ ]
|
||||||
|
66 -> 48 [ ]
|
||||||
|
67 -> 13 [ ]
|
||||||
|
67 -> 62 [ ]
|
||||||
|
68 -> 51 [ ]
|
||||||
|
70 -> 72 [ ]
|
||||||
|
71 -> 72 [ ]
|
||||||
|
72 -> 8 [ ]
|
||||||
|
72 -> 9 [ ]
|
||||||
|
72 -> 49 [ ]
|
||||||
|
72 -> 53 [ ]
|
||||||
|
72 -> 54 [ ]
|
||||||
|
74 -> 55 [ ]
|
||||||
|
74 -> 69 [ ]
|
||||||
|
74 -> 71 [ ]
|
||||||
|
74 -> 73 [ ]
|
||||||
|
74 -> 75 [ ]
|
||||||
|
75 -> 68 [ ]
|
||||||
|
75 -> 69 [ ]
|
||||||
|
75 -> 70 [ ]
|
||||||
|
75 -> 73 [ ]
|
||||||
|
|
||||||
|
}
|
1542
techtree.svg
Normal file
1542
techtree.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 103 KiB |
Loading…
Reference in a new issue