diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-18 08:27:36 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-18 08:27:36 +0800 |
| commit | 5d0918ff5e19ebc67e4698bc308f6562779b7110 (patch) | |
| tree | a9d7f5fb54afd16056b0a4c24d118db2d95f8d78 | |
| parent | 00cf68abdd2be1cfca8b5a268d087ea2ccedf1dd (diff) | |
feat: add new Mingling CI system crate (WIP)
Add `mingling_ci` as a standalone CI system built on Mingling 0.4.0
to validate the next version, with report collection and manifest
listing commands.
| -rw-r--r-- | .cargo/config.toml | 3 | ||||
| -rw-r--r-- | .config/ci-ignored-dirs.txt | 5 | ||||
| -rw-r--r-- | .vscode/settings.json | 1 | ||||
| -rw-r--r-- | .zed/settings.json | 1 | ||||
| -rw-r--r-- | mingling_ci/Cargo.lock | 560 | ||||
| -rw-r--r-- | mingling_ci/Cargo.toml | 41 | ||||
| -rw-r--r-- | mingling_ci/build.rs | 6 | ||||
| -rw-r--r-- | mingling_ci/help.txt | 11 | ||||
| -rw-r--r-- | mingling_ci/src/bin/ci.rs | 46 | ||||
| -rw-r--r-- | mingling_ci/src/cmd.rs | 2 | ||||
| -rw-r--r-- | mingling_ci/src/cmd/cmd_report_collect.rs | 293 | ||||
| -rw-r--r-- | mingling_ci/src/cmd/cmd_show_manifests.rs | 71 | ||||
| -rw-r--r-- | mingling_ci/src/job.rs | 1 | ||||
| -rw-r--r-- | mingling_ci/src/job/cmd_build.rs | 4 | ||||
| -rw-r--r-- | mingling_ci/src/lib.rs | 22 | ||||
| -rw-r--r-- | mingling_ci/src/reporter.rs | 108 | ||||
| -rw-r--r-- | mingling_ci/src/res.rs | 5 | ||||
| -rw-r--r-- | mingling_ci/src/res/manifests.rs | 100 | ||||
| -rw-r--r-- | mingling_ci/src/res/print.rs | 174 | ||||
| -rw-r--r-- | mingling_ci/tmpls/report.md | 9 | ||||
| -rw-r--r-- | mingling_ci/tmpls/task_section.md | 18 |
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>>> +``` + +@@@ <<< |
