aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cargo/config.toml3
-rw-r--r--.config/ci-ignored-dirs.txt5
-rw-r--r--.vscode/settings.json1
-rw-r--r--.zed/settings.json1
-rw-r--r--mingling_ci/Cargo.lock560
-rw-r--r--mingling_ci/Cargo.toml41
-rw-r--r--mingling_ci/build.rs6
-rw-r--r--mingling_ci/help.txt11
-rw-r--r--mingling_ci/src/bin/ci.rs46
-rw-r--r--mingling_ci/src/cmd.rs2
-rw-r--r--mingling_ci/src/cmd/cmd_report_collect.rs293
-rw-r--r--mingling_ci/src/cmd/cmd_show_manifests.rs71
-rw-r--r--mingling_ci/src/job.rs1
-rw-r--r--mingling_ci/src/job/cmd_build.rs4
-rw-r--r--mingling_ci/src/lib.rs22
-rw-r--r--mingling_ci/src/reporter.rs108
-rw-r--r--mingling_ci/src/res.rs5
-rw-r--r--mingling_ci/src/res/manifests.rs100
-rw-r--r--mingling_ci/src/res/print.rs174
-rw-r--r--mingling_ci/tmpls/report.md9
-rw-r--r--mingling_ci/tmpls/task_section.md18
21 files changed, 1481 insertions, 0 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index f375b03..bfc711f 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -6,3 +6,6 @@ target-dir = "./.temp/target"
[alias]
ci = "run --manifest-path .run/Cargo.toml --bin ci --quiet --"
dev_tool = "run --manifest-path .run/Cargo.toml --quiet --bin "
+
+# CI Next (Developing)
+cin = "run --manifest-path mingling_ci/Cargo.toml --bin ci --quiet --"
diff --git a/.config/ci-ignored-dirs.txt b/.config/ci-ignored-dirs.txt
new file mode 100644
index 0000000..08037c9
--- /dev/null
+++ b/.config/ci-ignored-dirs.txt
@@ -0,0 +1,5 @@
+# Temp
+./.temp/
+
+# Self
+./mingling_ci/
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 71ced58..6a905a8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,6 +4,7 @@
"rust-analyzer.files.exclude": ["**/target/**", "**/.temp/**"],
"rust-analyzer.linkedProjects": [
".run/Cargo.toml",
+ "mingling_ci/Cargo.toml",
"mingling_pathf/test/Cargo.toml",
"arg_picker/Cargo.toml",
"arg_picker/test/Cargo.toml",
diff --git a/.zed/settings.json b/.zed/settings.json
index 7378109..f4a55eb 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -9,6 +9,7 @@
},
"linkedProjects": [
".run/Cargo.toml",
+ "mingling_ci/Cargo.toml",
"mingling_pathf/test/Cargo.toml",
"arg_picker/Cargo.toml",
"arg_picker/test/Cargo.toml",
diff --git a/mingling_ci/Cargo.lock b/mingling_ci/Cargo.lock
new file mode 100644
index 0000000..5316e7a
--- /dev/null
+++ b/mingling_ci/Cargo.lock
@@ -0,0 +1,560 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arg-picker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e326de90a1c279562cc1583470daccfd4defe882661c61607edff75b797e483e"
+dependencies = [
+ "arg-picker-macros",
+ "just_fmt 0.2.1",
+]
+
+[[package]]
+name = "arg-picker-macros"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5f9bbe04b69744a30def8bee5526268b986616314bd1696804d7f819f03d1f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "colored"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "csv"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde_core",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_fmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b935090fce9a995a79798a22d523f1742b202f57ad2d8fcab6ad3dff528baf"
+
+[[package]]
+name = "just_progress"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef1a564328a5061a4828b4f82b7275a7f3dbc7d4ed5778da986f6ab48563c88"
+dependencies = [
+ "tokio",
+]
+
+[[package]]
+name = "just_template"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2a56a31287e7397340dd8d9997c8fd936ee68b27cb3f090ece2984e8a84d5bc"
+dependencies = [
+ "just_fmt 0.1.2",
+ "just_template_macros",
+]
+
+[[package]]
+name = "just_template_macros"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74f4328c632a33ef4c2c811e02bd2506f2db215bf7fbecbf934b0a61a17e7bea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libredox"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "might_be_async"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca871cea620b07bd2e6da0c883891a25bead698c43a9ab64b0fd663a7a78d5f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "toml",
+]
+
+[[package]]
+name = "mingling"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe3138d939ed9c987301b38a1f0577e4db5b0e9b14e16f73ebe37dad8e25380e"
+dependencies = [
+ "arg-picker",
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling-ci-system"
+version = "0.1.0"
+dependencies = [
+ "colored",
+ "just_progress",
+ "just_template",
+ "mingling",
+ "prettytable-rs",
+ "tokio",
+ "toml",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9165f58f51569b6627c09fd4292fa298c11ef2c467c0b77640a52339760249fb"
+dependencies = [
+ "just_fmt 0.2.1",
+ "might_be_async",
+ "mingling_pathf",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28cb7912933889738451b181d8bf958d215265555596328c5c4a8053c1be78d"
+dependencies = [
+ "just_fmt 0.2.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "mingling_pathf"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ed8d3adebe58b1c914f1fddce1b7a2afa2c9fca80665f0af251ea64662b8905"
+dependencies = [
+ "just_fmt 0.2.1",
+ "proc-macro2",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "prettytable-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
+dependencies = [
+ "csv",
+ "encode_unicode",
+ "is-terminal",
+ "lazy_static",
+ "term",
+ "unicode-width",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[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 2.0.119",
+]
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
diff --git a/mingling_ci/Cargo.toml b/mingling_ci/Cargo.toml
new file mode 100644
index 0000000..197bca3
--- /dev/null
+++ b/mingling_ci/Cargo.toml
@@ -0,0 +1,41 @@
+# This CI system is built on Mingling and is used to validate the next
+# version of Mingling.
+
+[package]
+name = "mingling-ci-system"
+edition = "2024"
+version = "0.1.0"
+publish = false
+
+[workspace]
+
+[dependencies]
+# NOTE: Do not use the current version of Mingling. The CI must be built on an
+# earlier version than the mainline Mingling.
+#
+# > You know the reason! ... the **grandfather paradox**
+mingling = { version = "0.4.0", features = [
+ "async",
+ "dispatch_tree",
+ "extras",
+ "pathf",
+ "picker",
+] }
+
+just_progress = "0.1.3"
+colored = "3.1.1"
+tokio = { version = "1.53.1", features = [
+ "rt",
+ "rt-multi-thread",
+ "macros",
+] }
+prettytable-rs = "0.10.0"
+toml = "0.8"
+just_template = "0.2.1"
+
+[build-dependencies]
+mingling = { version = "0.4.0", features = [
+ "build",
+ "dispatch_tree",
+ "pathf"
+] }
diff --git a/mingling_ci/build.rs b/mingling_ci/build.rs
new file mode 100644
index 0000000..e0bcc0c
--- /dev/null
+++ b/mingling_ci/build.rs
@@ -0,0 +1,6 @@
+use mingling::build::analyze_and_build_type_mapping;
+
+fn main() {
+ analyze_and_build_type_mapping().unwrap();
+}
+
diff --git a/mingling_ci/help.txt b/mingling_ci/help.txt
new file mode 100644
index 0000000..24563be
--- /dev/null
+++ b/mingling_ci/help.txt
@@ -0,0 +1,11 @@
+This program is used to check the code quality of the Mingling project itself.
+
+FLAGS:
+ -h, --help Print this help page
+ -q, --quiet Quiet output
+
+USAGE:
+ report-collect Collect and organize all inspection reports
+ report-clean Clean up all reports
+
+ show-manifests Print all crate paths that need to be checked
diff --git a/mingling_ci/src/bin/ci.rs b/mingling_ci/src/bin/ci.rs
new file mode 100644
index 0000000..a212921
--- /dev/null
+++ b/mingling_ci/src/bin/ci.rs
@@ -0,0 +1,46 @@
+use just_progress::{
+ progress::{self},
+ renderer::ProgressSimpleRenderer,
+};
+use mingling::{
+ hook::ProgramHook,
+ setup::{
+ ConfirmSetup, DirectoryEnvironmentSetup, ExitCodeSetup,
+ picker::{ConfirmFlagSetup, HelpFlagSetup, QuietFlagSetup},
+ },
+};
+use tokio::join;
+
+use mingling_ci_system::ThisProgram;
+use mingling_ci_system::res::*;
+
+#[tokio::main]
+async fn main() {
+ let center = progress::init();
+ let renderer = ProgressSimpleRenderer::new().with_subprogress(true);
+ let bind = progress::bind(center, move |name, state| renderer.update(name, state));
+
+ let (_, exit_code) = join!(bind, mingling_ci_begin());
+ std::process::exit(exit_code);
+}
+
+async fn mingling_ci_begin() -> i32 {
+ let mut program = ThisProgram::new();
+
+ program.with_hook(ProgramHook::empty().on_finish(|_| progress::close()));
+
+ // Plugins
+ program.with_setup(ExitCodeSetup::default());
+ program.with_setup(DirectoryEnvironmentSetup::default());
+
+ program.with_setup(HelpFlagSetup::default());
+ program.with_setup(ConfirmFlagSetup::default());
+ program.with_setup(QuietFlagSetup::default());
+
+ program.with_setup(ConfirmSetup);
+
+ // CI Plugins
+ program.with_setup(ManifestsSetup);
+
+ program.exec().await
+}
diff --git a/mingling_ci/src/cmd.rs b/mingling_ci/src/cmd.rs
new file mode 100644
index 0000000..45b077e
--- /dev/null
+++ b/mingling_ci/src/cmd.rs
@@ -0,0 +1,2 @@
+pub(crate) mod cmd_report_collect;
+pub(crate) mod cmd_show_manifests;
diff --git a/mingling_ci/src/cmd/cmd_report_collect.rs b/mingling_ci/src/cmd/cmd_report_collect.rs
new file mode 100644
index 0000000..da603c2
--- /dev/null
+++ b/mingling_ci/src/cmd/cmd_report_collect.rs
@@ -0,0 +1,293 @@
+use std::collections::{BTreeMap, HashMap};
+use std::path::PathBuf;
+
+use just_template::Template;
+use mingling::{
+ Grouped, RenderResult, Routable,
+ macros::{buffer, command, r_println, renderer},
+};
+
+use crate::Next;
+use crate::reporter::COLLECT_DIR;
+use crate::res::{CargoError, Manifests, MessagePrinter, package_name};
+
+const OUTPUT_PATH: &str = "./.temp/reports/result.md";
+const REPORT_TEMPLATE: &str = include_str!("../../tmpls/report.md");
+const TASK_SECTION_TEMPLATE: &str = include_str!("../../tmpls/task_section.md");
+
+/// Maps a package to its per-OS pass/fail status.
+type OsStatuses = BTreeMap<String, bool>;
+
+/// A row in a task section: package name and its per-OS statuses.
+type TaskRow<'a> = (&'a String, &'a OsStatuses);
+
+/// Rows grouped by task name.
+type RowsByTask<'a> = BTreeMap<&'a String, Vec<TaskRow<'a>>>;
+
+/// Git commit date and short hash for the report.
+struct GitInfo {
+ date: String,
+ commit_hash: String,
+}
+
+#[command(node = "report-collect")]
+pub fn report_collect(manifests: &Manifests) -> Next {
+ let Ok(task_entries) = std::fs::read_dir(COLLECT_DIR) else {
+ return ErrorNoCollectDir.to_chain();
+ };
+
+ // Parse `collect/{task}/{os}/{package}.{ok|err}` and group them by
+ // (task, package) -> os -> ok.
+ let mut statuses: BTreeMap<(String, String), OsStatuses> = BTreeMap::new();
+ for task_entry in task_entries.flatten() {
+ if !task_entry.file_type().is_ok_and(|t| t.is_dir()) {
+ continue;
+ }
+ let task = task_entry.file_name().to_string_lossy().into_owned();
+
+ let Ok(os_entries) = std::fs::read_dir(task_entry.path()) else {
+ continue;
+ };
+ for os_entry in os_entries.flatten() {
+ if !os_entry.file_type().is_ok_and(|t| t.is_dir()) {
+ continue;
+ }
+ let os = os_entry.file_name().to_string_lossy().into_owned();
+
+ let Ok(files) = std::fs::read_dir(os_entry.path()) else {
+ continue;
+ };
+ for file in files.flatten() {
+ let file_name = file.file_name().to_string_lossy().into_owned();
+ if let Some((package, ok)) = parse_log_name(&file_name) {
+ statuses
+ .entry((task.clone(), package))
+ .or_default()
+ .insert(os.clone(), ok);
+ }
+ }
+ }
+ }
+
+ // Group rows by task: task -> [(package, os_statuses)].
+ let by_task: RowsByTask = statuses.iter().fold(
+ BTreeMap::new(),
+ |mut acc, ((task, package), os_statuses)| {
+ acc.entry(task).or_default().push((package, os_statuses));
+ acc
+ },
+ );
+
+ // Render one section per task (table rows + this task's failures).
+ let mut fail_count = 0;
+ let mut sections: Vec<HashMap<String, String>> = Vec::new();
+ for (task, rows) in by_task {
+ let mut row_arms = Vec::new();
+ let mut fail_arms = Vec::new();
+ for (package, os_statuses) in rows {
+ row_arms.push(HashMap::from([
+ ("package_name".to_string(), package.clone()),
+ ("package_dir".to_string(), package_dir(manifests, package)),
+ (
+ "pass_win".to_string(),
+ pass_cell(os_statuses.get("Windows")),
+ ),
+ (
+ "pass_linux".to_string(),
+ pass_cell(os_statuses.get("Linux")),
+ ),
+ ("pass_mac".to_string(), pass_cell(os_statuses.get("MacOS"))),
+ ]));
+
+ for (os, ok) in os_statuses {
+ if !ok {
+ let path = format!("{COLLECT_DIR}/{task}/{os}/{package}.err");
+ let stdout = strip_ansi(&std::fs::read_to_string(path).unwrap_or_default());
+ fail_arms.push(HashMap::from([
+ ("package_name".to_string(), package.clone()),
+ ("stdout".to_string(), stdout),
+ ]));
+ fail_count += 1;
+ }
+ }
+ }
+
+ let mut section = Template::from(TASK_SECTION_TEMPLATE);
+ section.insert_param("task_name".to_string(), task.clone());
+ *section.add_impl("rows".to_string()) = row_arms;
+ *section.add_impl("fails".to_string()) = fail_arms;
+ sections.push(HashMap::from([(
+ "section".to_string(),
+ section.expand().unwrap_or_default(),
+ )]));
+ }
+
+ let git = git_info();
+ let mut template = Template::from(REPORT_TEMPLATE);
+ template.insert_param("date".to_string(), git.date);
+ template.insert_param("commit_hash".to_string(), git.commit_hash);
+ *template.add_impl("task_sections".to_string()) = sections;
+
+ let expanded = template.expand().unwrap_or_default();
+ std::fs::create_dir_all(std::path::Path::new(OUTPUT_PATH).parent().unwrap()).ok();
+ std::fs::write(OUTPUT_PATH, expanded).ok();
+
+ ResultCollectResults {
+ output: OUTPUT_PATH.into(),
+ fail_count,
+ }
+ .to_chain()
+}
+
+/// Parses a `{package}.{ok|err}` file name into `(package, ok)`.
+fn parse_log_name(file_name: &str) -> Option<(String, bool)> {
+ file_name
+ .strip_suffix(".ok")
+ .map(|n| (n.to_string(), true))
+ .or_else(|| {
+ file_name
+ .strip_suffix(".err")
+ .map(|n| (n.to_string(), false))
+ })
+}
+
+/// Strips ANSI escape sequences from `input`.
+///
+/// Handles CSI (`ESC [ ...`), OSC (`ESC ] ...` terminated by BEL or `ESC \`)
+/// and other single-character escapes, while preserving UTF-8 text. Literal
+/// `^[` (caret-bracket, produced by some terminal captures) is normalized to
+/// `ESC` first.
+fn strip_ansi(input: &str) -> String {
+ // Normalize literal `^[` (0x5E 0x5B) to a real ESC byte.
+ let normalized = input.replace("^[", "\u{1b}");
+ let mut out = String::with_capacity(normalized.len());
+ let mut rest = normalized.as_str();
+ while let Some(idx) = rest.find('\u{1b}') {
+ out.push_str(&rest[..idx]);
+ rest = &rest[idx..];
+ rest = &rest[ansi_len(rest)..];
+ }
+ out.push_str(rest);
+ out
+}
+
+/// Byte length of the ANSI escape sequence starting at `s[0]` (`s[0]` is `ESC`).
+fn ansi_len(s: &str) -> usize {
+ let b = s.as_bytes();
+ match b.get(1) {
+ Some(b'[') => {
+ // CSI: `ESC [` params/intermediates (0x20-0x3F) then a final byte (0x40-0x7E).
+ let mut i = 2;
+ while i < b.len() {
+ let byte = b[i];
+ i += 1;
+ if (0x40..=0x7E).contains(&byte) {
+ break;
+ }
+ if !(0x20..=0x3F).contains(&byte) {
+ break;
+ }
+ }
+ i
+ }
+ Some(b']') => {
+ // OSC: `ESC ]` ... terminated by BEL (0x07) or `ESC \`.
+ let mut i = 2;
+ while i < b.len() {
+ let byte = b[i];
+ i += 1;
+ if byte == 0x07 {
+ break;
+ }
+ if byte == 0x1b {
+ if b.get(i) == Some(&b'\\') {
+ i += 1;
+ }
+ break;
+ }
+ }
+ i
+ }
+ Some(_) => 2.min(b.len()),
+ None => 1,
+ }
+}
+
+/// Maps a package name to its manifest directory (e.g. `mingling` →
+/// `./mingling`), or `—` when the manifest is unknown.
+fn package_dir(manifests: &Manifests, package: &str) -> String {
+ manifests
+ .path
+ .iter()
+ .find(|path| package_name(path) == package)
+ .and_then(|path| path.parent())
+ .map_or_else(|| "—".to_string(), |dir| dir.to_string_lossy().into_owned())
+}
+
+fn pass_cell(status: Option<&bool>) -> String {
+ match status {
+ Some(true) => "✅".to_string(),
+ Some(false) => "❌".to_string(),
+ None => "—".to_string(),
+ }
+}
+
+/// Commit date (`YYYY-MM-DD`) and short commit hash; empty on failure.
+fn git_info() -> GitInfo {
+ let run = |args: &[&str]| {
+ std::process::Command::new("git")
+ .args(args)
+ .output()
+ .ok()
+ .filter(|o| o.status.success())
+ .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
+ .unwrap_or_default()
+ };
+ GitInfo {
+ date: run(&["log", "-1", "--format=%cs"]),
+ commit_hash: run(&["rev-parse", "--short", "HEAD"]),
+ }
+}
+
+/// The generated report.
+#[derive(Grouped)]
+pub struct ResultCollectResults {
+ pub output: PathBuf,
+ pub fail_count: usize,
+}
+
+#[derive(Grouped, Default)]
+pub struct ErrorNoCollectDir;
+
+#[renderer(buffer)]
+pub fn render_collect_results(r: ResultCollectResults) {
+ r_println!("Collected {} failing logs", r.fail_count);
+ r_println!("Report generated at {}", r.output.display());
+}
+
+#[renderer]
+pub fn render_error_no_collect_dir(_: ErrorNoCollectDir, error: &CargoError) -> RenderResult {
+ let render_result = RenderResult::new();
+ error.println(vec![format!("No collect directory: {COLLECT_DIR}")]);
+ render_result
+}
+
+#[cfg(test)]
+mod tests {
+ use super::strip_ansi;
+
+ #[test]
+ fn strips_csi_and_osc_and_literal_caret() {
+ let input =
+ "\u{1b}[1m\u{1b}[92mok\u{1b}[0m \u{1b}]8;;https://x\u{1b}\\done\u{1b}]8;;\u{1b}\\\n";
+ assert_eq!(strip_ansi(input), "ok done\n");
+
+ // Literal `^[` (caret-bracket) captured by some terminals.
+ assert_eq!(strip_ansi("^[[31mred^[[0m"), "red");
+ }
+
+ #[test]
+ fn preserves_utf8() {
+ assert_eq!(strip_ansi("你好\u{1b}[1m世界!\u{1b}[0m"), "你好世界!");
+ }
+}
diff --git a/mingling_ci/src/cmd/cmd_show_manifests.rs b/mingling_ci/src/cmd/cmd_show_manifests.rs
new file mode 100644
index 0000000..32e49c0
--- /dev/null
+++ b/mingling_ci/src/cmd/cmd_show_manifests.rs
@@ -0,0 +1,71 @@
+use std::path::PathBuf;
+
+use mingling::{
+ Grouped,
+ macros::{buffer, command, r_println, renderer},
+};
+
+use prettytable::{
+ Cell, Row, Table,
+ format::{FormatBuilder, LinePosition, LineSeparator},
+};
+
+use crate::res::{Manifests, package_name};
+
+#[command(node = "show-manifests")]
+pub fn show_manifests(manifests: &Manifests) -> ResultPrintManifests {
+ let mut entries: Vec<ManifestEntry> = manifests
+ .path
+ .iter()
+ .map(|path| ManifestEntry {
+ name: package_name(path),
+ path: path.clone(),
+ })
+ .collect();
+ entries.sort_by(|a, b| a.path.cmp(&b.path));
+ ResultPrintManifests { entries }
+}
+
+/// All manifests the CI will check, sorted by path.
+#[derive(Grouped)]
+pub struct ResultPrintManifests {
+ pub entries: Vec<ManifestEntry>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ManifestEntry {
+ pub name: String,
+ pub path: PathBuf,
+}
+
+#[renderer(buffer)]
+pub fn render_print_manifests(r: ResultPrintManifests) {
+ let mut table = Table::new();
+
+ table.set_format(
+ FormatBuilder::new()
+ .column_separator('│')
+ .borders('│')
+ .separator(LinePosition::Top, LineSeparator::new('─', '┬', '┌', '┐'))
+ .separator(LinePosition::Title, LineSeparator::new('─', '┼', '├', '┤'))
+ .separator(LinePosition::Bottom, LineSeparator::new('─', '┴', '└', '┘'))
+ .padding(1, 1)
+ .build(),
+ );
+
+ table.set_titles(Row::new(vec![
+ Cell::new("#"),
+ Cell::new("Package-Name"),
+ Cell::new("Package-Path"),
+ ]));
+
+ for (index, entry) in r.entries.iter().enumerate() {
+ table.add_row(Row::new(vec![
+ Cell::new(&(index + 1).to_string()),
+ Cell::new(&entry.name),
+ Cell::new(&entry.path.to_string_lossy()),
+ ]));
+ }
+
+ r_println!("{table}");
+}
diff --git a/mingling_ci/src/job.rs b/mingling_ci/src/job.rs
new file mode 100644
index 0000000..6d86ac6
--- /dev/null
+++ b/mingling_ci/src/job.rs
@@ -0,0 +1 @@
+pub(crate) mod cmd_build;
diff --git a/mingling_ci/src/job/cmd_build.rs b/mingling_ci/src/job/cmd_build.rs
new file mode 100644
index 0000000..7f43102
--- /dev/null
+++ b/mingling_ci/src/job/cmd_build.rs
@@ -0,0 +1,4 @@
+use mingling::macros::command;
+
+#[command(node = "build")]
+pub fn build() {}
diff --git a/mingling_ci/src/lib.rs b/mingling_ci/src/lib.rs
new file mode 100644
index 0000000..b625a81
--- /dev/null
+++ b/mingling_ci/src/lib.rs
@@ -0,0 +1,22 @@
+#![deny(clippy::pedantic)]
+#![deny(clippy::nursery)]
+#![allow(clippy::redundant_pub_crate)]
+#![allow(clippy::missing_const_for_fn)]
+
+use mingling::macros::{gen_program, help};
+
+pub(crate) mod cmd;
+pub(crate) mod job;
+
+/// Mingling CI's Resources
+pub mod res;
+
+/// Log exporter for CI reports
+pub mod reporter;
+
+#[help]
+pub fn render_fallback(_: EntryFallback) -> String {
+ include_str!("../help.txt").to_string()
+}
+
+gen_program!();
diff --git a/mingling_ci/src/reporter.rs b/mingling_ci/src/reporter.rs
new file mode 100644
index 0000000..00ce3e5
--- /dev/null
+++ b/mingling_ci/src/reporter.rs
@@ -0,0 +1,108 @@
+//! Minimal log exporter for CI reports.
+//!
+//! Writes per-package results into `collect/{task}/{platform}/{package}.{ok|err}`
+//! so that the [`crate::cmd::collect_results`] command can assemble the final
+//! report. The task name is set once per CI phase via [`set_task`].
+
+use std::fs;
+use std::path::Path;
+use std::sync::Mutex;
+
+/// Root of the collected CI logs (relative to the repo root).
+pub const COLLECT_DIR: &str = "./.temp/reports/collect";
+
+/// The platform a package check ran on.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ReportPlatform {
+ Windows,
+ Linux,
+ MacOS,
+}
+
+impl ReportPlatform {
+ /// Directory name used under the task folder.
+ const fn dir_name(self) -> &'static str {
+ match self {
+ Self::Windows => "Windows",
+ Self::Linux => "Linux",
+ Self::MacOS => "MacOS",
+ }
+ }
+}
+
+/// The outcome of a package check.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ReportResult {
+ /// Check passed.
+ Ok,
+ /// Check failed, with the captured output.
+ Error(String),
+}
+
+/// Current task name (e.g. `Build-All`); set via [`set_task`].
+static CURRENT_TASK: Mutex<Option<String>> = Mutex::new(None);
+
+/// Sets the task that subsequent [`export`] calls write under.
+///
+/// # Panics
+///
+/// Panics if the internal mutex is poisoned.
+pub fn set_task(task: &str) {
+ *CURRENT_TASK.lock().unwrap() = Some(task.to_string());
+}
+
+/// Exports one package result to `collect/{task}/{platform}/{package}.{ok|err}`.
+///
+/// Writes `{package}.ok` on success and `{package}.err` (with the output) on
+/// failure. Errors are reported to stderr and otherwise ignored.
+///
+/// # Panics
+///
+/// Panics if the internal task mutex is poisoned.
+pub fn export(package: &str, platform: ReportPlatform, result: ReportResult) {
+ let Some(task) = CURRENT_TASK.lock().unwrap().clone() else {
+ eprintln!("reporter: no current task; call reporter::set_task first");
+ return;
+ };
+
+ let dir = Path::new(COLLECT_DIR).join(task).join(platform.dir_name());
+ if let Err(e) = fs::create_dir_all(&dir) {
+ eprintln!("reporter: failed to create {}: {e}", dir.display());
+ return;
+ }
+
+ let (file_name, content) = match result {
+ ReportResult::Ok => (format!("{package}.ok"), String::new()),
+ ReportResult::Error(output) => (format!("{package}.err"), output),
+ };
+ let path = dir.join(file_name);
+ if let Err(e) = fs::write(&path, content) {
+ eprintln!("reporter: failed to write {}: {e}", path.display());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn export_writes_ok_and_err_files() {
+ set_task("reporter-test");
+ let task_root = Path::new(COLLECT_DIR).join("reporter-test");
+ let dir = task_root.join("Linux");
+ fs::remove_dir_all(&task_root).ok();
+
+ export("pkg-a", ReportPlatform::Linux, ReportResult::Ok);
+ export(
+ "pkg-b",
+ ReportPlatform::Linux,
+ ReportResult::Error("boom".to_string()),
+ );
+
+ assert!(dir.join("pkg-a.ok").is_file());
+ assert!(dir.join("pkg-b.err").is_file());
+ assert_eq!(fs::read_to_string(dir.join("pkg-b.err")).unwrap(), "boom");
+
+ fs::remove_dir_all(&task_root).ok();
+ }
+}
diff --git a/mingling_ci/src/res.rs b/mingling_ci/src/res.rs
new file mode 100644
index 0000000..5b33a8f
--- /dev/null
+++ b/mingling_ci/src/res.rs
@@ -0,0 +1,5 @@
+mod manifests;
+pub use manifests::*;
+
+mod print;
+pub use print::*;
diff --git a/mingling_ci/src/res/manifests.rs b/mingling_ci/src/res/manifests.rs
new file mode 100644
index 0000000..56829bb
--- /dev/null
+++ b/mingling_ci/src/res/manifests.rs
@@ -0,0 +1,100 @@
+use std::path::{Path, PathBuf};
+
+use mingling::{Program, macros::program_setup};
+
+use crate::ThisProgram;
+
+/// Directories whose manifests are excluded from CI checks.
+///
+/// Path is relative to the crate source file (`mingling_ci/src/res/`).
+const IGNORED_DIRS_FILE: &str = include_str!("../../../.config/ci-ignored-dirs.txt");
+
+/// All `Cargo.toml` manifests the CI will check.
+#[derive(Default, Clone)]
+pub struct Manifests {
+ pub path: Vec<PathBuf>,
+}
+
+#[program_setup]
+pub fn manifests_setup(p: &mut Program<ThisProgram>) {
+ p.with_resource(Manifests {
+ path: cargo_tomls(),
+ });
+}
+
+/// Recursively collects every `Cargo.toml` under the current directory,
+/// skipping the legacy `.run` CI directory and any directory listed in
+/// `.config/ci-ignored-dirs.txt`.
+#[must_use]
+fn cargo_tomls() -> Vec<PathBuf> {
+ let ignored = ignored_dirs();
+ let mut cargo_tomls = Vec::new();
+ let mut dirs = vec![PathBuf::from(".")];
+ while let Some(dir) = dirs.pop() {
+ if is_ignored(&dir.to_string_lossy(), &ignored) {
+ continue;
+ }
+ if let Ok(entries) = std::fs::read_dir(&dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ // Skip the legacy `.run` CI directory
+ if path.file_name().and_then(|n| n.to_str()) == Some(".run") {
+ continue;
+ }
+ dirs.push(path);
+ } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
+ cargo_tomls.push(path);
+ }
+ }
+ }
+ }
+ cargo_tomls
+}
+
+/// Parses `.config/ci-ignored-dirs.txt` into directory prefixes:
+/// non-empty lines that do not start with `#`, with the trailing `/` stripped
+/// (e.g. `./.temp/` → `./.temp`).
+fn ignored_dirs() -> Vec<String> {
+ IGNORED_DIRS_FILE
+ .lines()
+ .map(str::trim)
+ .filter(|line| !line.is_empty() && !line.starts_with('#'))
+ .map(|line| line.trim_end_matches('/').to_string())
+ .collect()
+}
+
+/// Whether `path` (a walk directory, e.g. `./.temp` or `./examples`) is inside
+/// one of the ignored directories.
+fn is_ignored(path: &str, ignored: &[String]) -> bool {
+ ignored.iter().any(|dir| {
+ path.strip_prefix(dir.as_str())
+ .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
+ })
+}
+
+/// Extracts the package name from a `Cargo.toml`.
+///
+/// Falls back to the parent directory name (e.g. `mingling_core/Cargo.toml` →
+/// `mingling_core`, workspace root → `(root)`), matching the legacy CI.
+pub(crate) fn package_name(path: &Path) -> String {
+ let fallback = || {
+ path.parent()
+ .and_then(|p| p.file_name())
+ .and_then(|n| n.to_str())
+ .unwrap_or("(root)")
+ .to_string()
+ };
+
+ let Ok(content) = std::fs::read_to_string(path) else {
+ return fallback();
+ };
+ let Ok(toml_value) = content.parse::<toml::Table>() else {
+ return fallback();
+ };
+ toml_value
+ .get("package")
+ .and_then(|p| p.get("name"))
+ .and_then(|n| n.as_str())
+ .map_or_else(fallback, str::to_string)
+}
diff --git a/mingling_ci/src/res/print.rs b/mingling_ci/src/res/print.rs
new file mode 100644
index 0000000..9d844a6
--- /dev/null
+++ b/mingling_ci/src/res/print.rs
@@ -0,0 +1,174 @@
+use colored::Colorize;
+use mingling::config::ErrorOutput;
+use mingling::hook::ProgramHook;
+use mingling::{Program, macros::program_setup};
+use mingling::{StringVec, this};
+
+use crate::ThisProgram;
+
+#[program_setup]
+pub fn print_setup(p: &mut Program<ThisProgram>) {
+ p.with_resource(CargoError::default());
+ p.with_resource(CargoWarn::default());
+ p.with_resource(CargoHelp::default());
+ p.with_resource(CargoStatus::default());
+
+ p.with_hook(ProgramHook::empty().on_begin::<_, ()>(move |_| {
+ let p = this::<ThisProgram>();
+ let silence_err = p.stdout_setting.error_output == ErrorOutput::Hide;
+
+ p.modify_res(|r: &mut CargoError| r.silence = silence_err);
+ p.modify_res(|r: &mut CargoWarn| r.silence = silence_err);
+ p.modify_res(|r: &mut CargoHelp| r.silence = silence_err);
+ p.modify_res(|r: &mut CargoStatus| r.silence = silence_err);
+ }));
+}
+
+#[derive(Default, Clone)]
+pub struct CargoError {
+ silence: bool,
+}
+
+impl MessagePrinter for CargoError {
+ fn format(&self, msg: impl Into<StringVec>) -> String {
+ format!("{}: {}", "error".bold().bright_red(), msg.into().join(""))
+ }
+
+ fn std_mode(&self) -> StandardOutMode {
+ if self.silence {
+ StandardOutMode::Silence
+ } else {
+ StandardOutMode::Error
+ }
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct CargoWarn {
+ silence: bool,
+}
+
+impl MessagePrinter for CargoWarn {
+ fn format(&self, msg: impl Into<StringVec>) -> String {
+ format!("{}: {}", "warning".bright_yellow(), msg.into().join(""))
+ }
+
+ fn std_mode(&self) -> StandardOutMode {
+ if self.silence {
+ StandardOutMode::Silence
+ } else {
+ StandardOutMode::Error
+ }
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct CargoHelp {
+ silence: bool,
+}
+
+impl MessagePrinter for CargoHelp {
+ fn format(&self, msg: impl Into<StringVec>) -> String {
+ format!("{}: {}", "help".bright_white(), msg.into().join(""))
+ }
+
+ fn std_mode(&self) -> StandardOutMode {
+ if self.silence {
+ StandardOutMode::Silence
+ } else {
+ StandardOutMode::Error
+ }
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct CargoStatus {
+ silence: bool,
+}
+
+impl MessagePrinter for CargoStatus {
+ fn format(&self, msg: impl Into<StringVec>) -> String {
+ let parts: Vec<String> = msg.into().to_vec();
+ let first = if parts.is_empty() {
+ String::new()
+ } else {
+ parts[0].trim().to_string()
+ };
+
+ let (prefix, content) = if first.is_empty() {
+ // Empty: fall back to Info with full message
+ ("Info".to_string(), parts.join(" "))
+ } else if first.chars().count() == 1 {
+ // Single character: prefix is Info, entire message is content
+ ("Info".to_string(), parts.join(" "))
+ } else if first.chars().count() <= 12 {
+ // Single part that is a status prefix (no message after it)
+ if parts.len() == 1 {
+ ("Info".to_string(), first)
+ } else {
+ // First part is a status prefix, remaining parts are the message
+ let content = parts[1..].join(" ").trim_start().to_string();
+ (first, content)
+ }
+ } else {
+ // First part too long: all is message, fall back to Info
+ ("Info".to_string(), parts.join(" "))
+ };
+
+ let padding = " ".repeat(12usize.saturating_sub(prefix.chars().count()));
+
+ format!(
+ "{}{} {}",
+ padding,
+ prefix.bold().bright_green(),
+ content.trim()
+ )
+ }
+
+ fn std_mode(&self) -> StandardOutMode {
+ if self.silence {
+ StandardOutMode::Silence
+ } else {
+ StandardOutMode::Out
+ }
+ }
+}
+
+pub trait MessagePrinter {
+ #[doc(hidden)]
+ fn println(&self, msg: impl Into<StringVec>) {
+ match self.std_mode() {
+ StandardOutMode::Out => println!("{}", self.format(msg)),
+ StandardOutMode::Error => eprintln!("{}", self.format(msg)),
+ StandardOutMode::Silence => {}
+ }
+ }
+
+ #[doc(hidden)]
+ fn print(&self, msg: impl Into<StringVec>) {
+ match self.std_mode() {
+ StandardOutMode::Out => print!("{}", self.format(msg)),
+ StandardOutMode::Error => eprint!("{}", self.format(msg)),
+ StandardOutMode::Silence => {}
+ }
+ }
+
+ /// Formats the message string before output.
+ fn format(&self, msg: impl Into<StringVec>) -> String;
+
+ /// Returns the standard output mode (stdout or stderr).
+ fn std_mode(&self) -> StandardOutMode;
+}
+
+/// Specifies where standard output messages should be directed.
+///
+/// This enum determines whether messages are printed to stdout, stderr, or suppressed entirely.
+#[repr(u8)]
+pub enum StandardOutMode {
+ /// Print messages to standard output (stdout).
+ Out,
+ /// Print messages to standard error (stderr).
+ Error,
+ /// Suppress all output.
+ Silence,
+}
diff --git a/mingling_ci/tmpls/report.md b/mingling_ci/tmpls/report.md
new file mode 100644
index 0000000..020fda2
--- /dev/null
+++ b/mingling_ci/tmpls/report.md
@@ -0,0 +1,9 @@
+<h1 align="center">Mingling CI Results</h1>
+
+<p align="center"> <<<date>>> - <<<commit_hash>>> </p>
+
+>>>>>>>>>> task_sections
+@@@ >>> task_sections
+<<<section>>>
+
+@@@ <<<
diff --git a/mingling_ci/tmpls/task_section.md b/mingling_ci/tmpls/task_section.md
new file mode 100644
index 0000000..9c6daad
--- /dev/null
+++ b/mingling_ci/tmpls/task_section.md
@@ -0,0 +1,18 @@
+## Task: <<<task_name>>>
+
+| Package-Name | Package-Directory | PASS (Windows) | PASS (Linux) | PASS (Mac OS) |
+| -------------- | ----------------- | -------------- | ------------ | ------------- |
+>>>>>>>>>> rows
+@@@ >>> rows
+| <<<package_name>>> | <<<package_dir>>> | <<<pass_win>>> | <<<pass_linux>>> | <<<pass_mac>>> |
+@@@ <<<
+
+>>>>>>>>>> fails
+@@@ >>> fails
+### Fail: #<<<package_name>>>
+
+```stdout
+<<<stdout>>>
+```
+
+@@@ <<<