collin/anvil · b26efa9c
Add CI foundation (pipeline + run model + push trigger); switch SSH crypto to ring
Collin Richards · 2026-06-09 20:36 UTC · b26efa9c0a4475d903149a1c8169e36b4dd081ec · parent 67ea942a · browse files
modifiedCargo.lock+124 −84
| ⋯ 104 unchanged lines | |||
| 105 | 105 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 106 | 106 | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
| 107 | 107 | dependencies = [ | |
| 108 | - | "windows-sys", | |
| 108 | + | "windows-sys 0.61.2", | |
| 109 | 109 | ] | |
| 110 | 110 | ||
| 111 | 111 | [[package]] | |
| ⋯ 4 unchanged lines | |||
| 116 | 116 | dependencies = [ | |
| 117 | 117 | "anstyle", | |
| 118 | 118 | "once_cell_polyfill", | |
| 119 | - | "windows-sys", | |
| 119 | + | "windows-sys 0.61.2", | |
| 120 | 120 | ] | |
| 121 | 121 | ||
| 122 | 122 | [[package]] | |
| ⋯ 17 unchanged lines | |||
| 140 | 140 | "argon2 0.5.3", | |
| 141 | 141 | "gix", | |
| 142 | 142 | "serde", | |
| 143 | + | "serde_yaml", | |
| 143 | 144 | "ssh-key", | |
| 144 | 145 | "tempfile", | |
| 145 | 146 | "thiserror", | |
| ⋯ 11 unchanged lines | |||
| 157 | 158 | "gitserver-core", | |
| 158 | 159 | "gix", | |
| 159 | 160 | "thiserror", | |
| 161 | + | "toasty", | |
| 160 | 162 | "tokio", | |
| 161 | 163 | "tracing", | |
| 162 | 164 | ] | |
| ⋯ 99 unchanged lines | |||
| 262 | 264 | checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | |
| 263 | 265 | ||
| 264 | 266 | [[package]] | |
| 265 | - | name = "aws-lc-rs" | |
| 266 | - | version = "1.17.0" | |
| 267 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 268 | - | checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" | |
| 269 | - | dependencies = [ | |
| 270 | - | "aws-lc-sys", | |
| 271 | - | "untrusted", | |
| 272 | - | "zeroize", | |
| 273 | - | ] | |
| 274 | - | ||
| 275 | - | [[package]] | |
| 276 | - | name = "aws-lc-sys" | |
| 277 | - | version = "0.41.0" | |
| 278 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 279 | - | checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" | |
| 280 | - | dependencies = [ | |
| 281 | - | "cc", | |
| 282 | - | "cmake", | |
| 283 | - | "dunce", | |
| 284 | - | "fs_extra", | |
| 285 | - | ] | |
| 286 | - | ||
| 287 | - | [[package]] | |
| 288 | 267 | name = "axum" | |
| 289 | 268 | version = "0.8.9" | |
| 290 | 269 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 252 unchanged lines | |||
| 543 | 522 | checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" | |
| 544 | 523 | dependencies = [ | |
| 545 | 524 | "find-msvc-tools", | |
| 546 | - | "jobserver", | |
| 547 | - | "libc", | |
| 548 | 525 | "shlex", | |
| 549 | 526 | ] | |
| 550 | 527 | ||
| ⋯ 97 unchanged lines | |||
| 648 | 625 | ] | |
| 649 | 626 | ||
| 650 | 627 | [[package]] | |
| 651 | - | name = "cmake" | |
| 652 | - | version = "0.1.58" | |
| 653 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 654 | - | checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" | |
| 655 | - | dependencies = [ | |
| 656 | - | "cc", | |
| 657 | - | ] | |
| 658 | - | ||
| 659 | - | [[package]] | |
| 660 | 628 | name = "cmov" | |
| 661 | 629 | version = "0.5.4" | |
| 662 | 630 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 390 unchanged lines | |||
| 1053 | 1021 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 1054 | 1022 | dependencies = [ | |
| 1055 | 1023 | "libc", | |
| 1056 | - | "windows-sys", | |
| 1024 | + | "windows-sys 0.61.2", | |
| 1057 | 1025 | ] | |
| 1058 | 1026 | ||
| 1059 | 1027 | [[package]] | |
| ⋯ 105 unchanged lines | |||
| 1165 | 1133 | ] | |
| 1166 | 1134 | ||
| 1167 | 1135 | [[package]] | |
| 1168 | - | name = "fs_extra" | |
| 1169 | - | version = "1.3.0" | |
| 1170 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1171 | - | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" | |
| 1172 | - | ||
| 1173 | - | [[package]] | |
| 1174 | 1136 | name = "futures" | |
| 1175 | 1137 | version = "0.3.32" | |
| 1176 | 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 115 unchanged lines | |||
| 1292 | 1254 | ||
| 1293 | 1255 | [[package]] | |
| 1294 | 1256 | name = "getrandom" | |
| 1295 | - | version = "0.3.4" | |
| 1296 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1297 | - | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | |
| 1298 | - | dependencies = [ | |
| 1299 | - | "cfg-if", | |
| 1300 | - | "libc", | |
| 1301 | - | "r-efi 5.3.0", | |
| 1302 | - | "wasip2", | |
| 1303 | - | ] | |
| 1304 | - | ||
| 1305 | - | [[package]] | |
| 1306 | - | name = "getrandom" | |
| 1307 | 1257 | version = "0.4.2" | |
| 1308 | 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1309 | 1259 | checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" | |
| ⋯ 1 unchanged line | |||
| 1311 | 1261 | "cfg-if", | |
| 1312 | 1262 | "js-sys", | |
| 1313 | 1263 | "libc", | |
| 1314 | - | "r-efi 6.0.0", | |
| 1264 | + | "r-efi", | |
| 1315 | 1265 | "rand_core 0.10.1", | |
| 1316 | 1266 | "wasip2", | |
| 1317 | 1267 | "wasip3", | |
| ⋯ 604 unchanged lines | |||
| 1922 | 1872 | "bitflags", | |
| 1923 | 1873 | "gix-path", | |
| 1924 | 1874 | "libc", | |
| 1925 | - | "windows-sys", | |
| 1875 | + | "windows-sys 0.61.2", | |
| 1926 | 1876 | ] | |
| 1927 | 1877 | ||
| 1928 | 1878 | [[package]] | |
| ⋯ 592 unchanged lines | |||
| 2521 | 2471 | checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" | |
| 2522 | 2472 | dependencies = [ | |
| 2523 | 2473 | "jiff-tzdb", | |
| 2524 | - | ] | |
| 2525 | - | ||
| 2526 | - | [[package]] | |
| 2527 | - | name = "jobserver" | |
| 2528 | - | version = "0.1.34" | |
| 2529 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2530 | - | checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" | |
| 2531 | - | dependencies = [ | |
| 2532 | - | "getrandom 0.3.4", | |
| 2533 | - | "libc", | |
| 2534 | 2474 | ] | |
| 2535 | 2475 | ||
| 2536 | 2476 | [[package]] | |
| ⋯ 209 unchanged lines | |||
| 2746 | 2686 | dependencies = [ | |
| 2747 | 2687 | "libc", | |
| 2748 | 2688 | "wasi", | |
| 2749 | - | "windows-sys", | |
| 2689 | + | "windows-sys 0.61.2", | |
| 2750 | 2690 | ] | |
| 2751 | 2691 | ||
| 2752 | 2692 | [[package]] | |
| ⋯ 45 unchanged lines | |||
| 2798 | 2738 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2799 | 2739 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 2800 | 2740 | dependencies = [ | |
| 2801 | - | "windows-sys", | |
| 2741 | + | "windows-sys 0.61.2", | |
| 2802 | 2742 | ] | |
| 2803 | 2743 | ||
| 2804 | 2744 | [[package]] | |
| ⋯ 396 unchanged lines | |||
| 3201 | 3141 | dependencies = [ | |
| 3202 | 3142 | "proc-macro2", | |
| 3203 | 3143 | ] | |
| 3204 | - | ||
| 3205 | - | [[package]] | |
| 3206 | - | name = "r-efi" | |
| 3207 | - | version = "5.3.0" | |
| 3208 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3209 | - | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | |
| 3210 | 3144 | ||
| 3211 | 3145 | [[package]] | |
| 3212 | 3146 | name = "r-efi" | |
| ⋯ 76 unchanged lines | |||
| 3289 | 3223 | ] | |
| 3290 | 3224 | ||
| 3291 | 3225 | [[package]] | |
| 3226 | + | name = "ring" | |
| 3227 | + | version = "0.17.14" | |
| 3228 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3229 | + | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" | |
| 3230 | + | dependencies = [ | |
| 3231 | + | "cc", | |
| 3232 | + | "cfg-if", | |
| 3233 | + | "getrandom 0.2.17", | |
| 3234 | + | "libc", | |
| 3235 | + | "untrusted", | |
| 3236 | + | "windows-sys 0.52.0", | |
| 3237 | + | ] | |
| 3238 | + | ||
| 3239 | + | [[package]] | |
| 3292 | 3240 | name = "rsa" | |
| 3293 | 3241 | version = "0.10.0-rc.18" | |
| 3294 | 3242 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 44 unchanged lines | |||
| 3339 | 3287 | checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0" | |
| 3340 | 3288 | dependencies = [ | |
| 3341 | 3289 | "aes", | |
| 3342 | - | "aws-lc-rs", | |
| 3343 | 3290 | "bitflags", | |
| 3344 | 3291 | "block-padding", | |
| 3345 | 3292 | "byteorder", | |
| ⋯ 37 unchanged lines | |||
| 3383 | 3330 | "polyval", | |
| 3384 | 3331 | "rand", | |
| 3385 | 3332 | "rand_core 0.10.1", | |
| 3333 | + | "ring", | |
| 3386 | 3334 | "rsa", | |
| 3387 | 3335 | "russh-cryptovec", | |
| 3388 | 3336 | "russh-util", | |
| ⋯ 24 unchanged lines | |||
| 3413 | 3361 | "log", | |
| 3414 | 3362 | "nix", | |
| 3415 | 3363 | "ssh-encoding", | |
| 3416 | - | "windows-sys", | |
| 3364 | + | "windows-sys 0.61.2", | |
| 3417 | 3365 | ] | |
| 3418 | 3366 | ||
| 3419 | 3367 | [[package]] | |
| ⋯ 27 unchanged lines | |||
| 3447 | 3395 | "errno", | |
| 3448 | 3396 | "libc", | |
| 3449 | 3397 | "linux-raw-sys", | |
| 3450 | - | "windows-sys", | |
| 3398 | + | "windows-sys 0.61.2", | |
| 3451 | 3399 | ] | |
| 3452 | 3400 | ||
| 3453 | 3401 | [[package]] | |
| ⋯ 141 unchanged lines | |||
| 3595 | 3543 | ] | |
| 3596 | 3544 | ||
| 3597 | 3545 | [[package]] | |
| 3546 | + | name = "serde_yaml" | |
| 3547 | + | version = "0.9.34+deprecated" | |
| 3548 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3549 | + | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" | |
| 3550 | + | dependencies = [ | |
| 3551 | + | "indexmap", | |
| 3552 | + | "itoa", | |
| 3553 | + | "ryu", | |
| 3554 | + | "serde", | |
| 3555 | + | "unsafe-libyaml", | |
| 3556 | + | ] | |
| 3557 | + | ||
| 3558 | + | [[package]] | |
| 3598 | 3559 | name = "serdect" | |
| 3599 | 3560 | version = "0.4.3" | |
| 3600 | 3561 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 128 unchanged lines | |||
| 3729 | 3690 | checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" | |
| 3730 | 3691 | dependencies = [ | |
| 3731 | 3692 | "libc", | |
| 3732 | - | "windows-sys", | |
| 3693 | + | "windows-sys 0.61.2", | |
| 3733 | 3694 | ] | |
| 3734 | 3695 | ||
| 3735 | 3696 | [[package]] | |
| ⋯ 162 unchanged lines | |||
| 3898 | 3859 | "getrandom 0.4.2", | |
| 3899 | 3860 | "once_cell", | |
| 3900 | 3861 | "rustix", | |
| 3901 | - | "windows-sys", | |
| 3862 | + | "windows-sys 0.61.2", | |
| 3902 | 3863 | ] | |
| 3903 | 3864 | ||
| 3904 | 3865 | [[package]] | |
| ⋯ 177 unchanged lines | |||
| 4082 | 4043 | "signal-hook-registry", | |
| 4083 | 4044 | "socket2", | |
| 4084 | 4045 | "tokio-macros", | |
| 4085 | - | "windows-sys", | |
| 4046 | + | "windows-sys 0.61.2", | |
| 4086 | 4047 | ] | |
| 4087 | 4048 | ||
| 4088 | 4049 | [[package]] | |
| ⋯ 247 unchanged lines | |||
| 4336 | 4297 | ] | |
| 4337 | 4298 | ||
| 4338 | 4299 | [[package]] | |
| 4300 | + | name = "unsafe-libyaml" | |
| 4301 | + | version = "0.2.11" | |
| 4302 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4303 | + | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" | |
| 4304 | + | ||
| 4305 | + | [[package]] | |
| 4339 | 4306 | name = "untrusted" | |
| 4340 | - | version = "0.7.1" | |
| 4307 | + | version = "0.9.0" | |
| 4341 | 4308 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4342 | - | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" | |
| 4309 | + | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | |
| 4343 | 4310 | ||
| 4344 | 4311 | [[package]] | |
| 4345 | 4312 | name = "url" | |
| ⋯ 178 unchanged lines | |||
| 4524 | 4491 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4525 | 4492 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 4526 | 4493 | dependencies = [ | |
| 4527 | - | "windows-sys", | |
| 4494 | + | "windows-sys 0.61.2", | |
| 4528 | 4495 | ] | |
| 4529 | 4496 | ||
| 4530 | 4497 | [[package]] | |
| ⋯ 99 unchanged lines | |||
| 4630 | 4597 | ||
| 4631 | 4598 | [[package]] | |
| 4632 | 4599 | name = "windows-sys" | |
| 4600 | + | version = "0.52.0" | |
| 4601 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4602 | + | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | |
| 4603 | + | dependencies = [ | |
| 4604 | + | "windows-targets", | |
| 4605 | + | ] | |
| 4606 | + | ||
| 4607 | + | [[package]] | |
| 4608 | + | name = "windows-sys" | |
| 4633 | 4609 | version = "0.61.2" | |
| 4634 | 4610 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4635 | 4611 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| ⋯ 2 unchanged lines | |||
| 4638 | 4614 | ] | |
| 4639 | 4615 | ||
| 4640 | 4616 | [[package]] | |
| 4617 | + | name = "windows-targets" | |
| 4618 | + | version = "0.52.6" | |
| 4619 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4620 | + | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | |
| 4621 | + | dependencies = [ | |
| 4622 | + | "windows_aarch64_gnullvm", | |
| 4623 | + | "windows_aarch64_msvc", | |
| 4624 | + | "windows_i686_gnu", | |
| 4625 | + | "windows_i686_gnullvm", | |
| 4626 | + | "windows_i686_msvc", | |
| 4627 | + | "windows_x86_64_gnu", | |
| 4628 | + | "windows_x86_64_gnullvm", | |
| 4629 | + | "windows_x86_64_msvc", | |
| 4630 | + | ] | |
| 4631 | + | ||
| 4632 | + | [[package]] | |
| 4641 | 4633 | name = "windows-threading" | |
| 4642 | 4634 | version = "0.2.1" | |
| 4643 | 4635 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 3 unchanged lines | |||
| 4647 | 4639 | ] | |
| 4648 | 4640 | ||
| 4649 | 4641 | [[package]] | |
| 4642 | + | name = "windows_aarch64_gnullvm" | |
| 4643 | + | version = "0.52.6" | |
| 4644 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4645 | + | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | |
| 4646 | + | ||
| 4647 | + | [[package]] | |
| 4648 | + | name = "windows_aarch64_msvc" | |
| 4649 | + | version = "0.52.6" | |
| 4650 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4651 | + | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | |
| 4652 | + | ||
| 4653 | + | [[package]] | |
| 4654 | + | name = "windows_i686_gnu" | |
| 4655 | + | version = "0.52.6" | |
| 4656 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4657 | + | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | |
| 4658 | + | ||
| 4659 | + | [[package]] | |
| 4660 | + | name = "windows_i686_gnullvm" | |
| 4661 | + | version = "0.52.6" | |
| 4662 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4663 | + | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | |
| 4664 | + | ||
| 4665 | + | [[package]] | |
| 4666 | + | name = "windows_i686_msvc" | |
| 4667 | + | version = "0.52.6" | |
| 4668 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4669 | + | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | |
| 4670 | + | ||
| 4671 | + | [[package]] | |
| 4672 | + | name = "windows_x86_64_gnu" | |
| 4673 | + | version = "0.52.6" | |
| 4674 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4675 | + | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | |
| 4676 | + | ||
| 4677 | + | [[package]] | |
| 4678 | + | name = "windows_x86_64_gnullvm" | |
| 4679 | + | version = "0.52.6" | |
| 4680 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4681 | + | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | |
| 4682 | + | ||
| 4683 | + | [[package]] | |
| 4684 | + | name = "windows_x86_64_msvc" | |
| 4685 | + | version = "0.52.6" | |
| 4686 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4687 | + | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |
| 4688 | + | ||
| 4689 | + | [[package]] | |
| 4650 | 4690 | name = "winnow" | |
| 4651 | 4691 | version = "0.7.15" | |
| 4652 | 4692 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| ⋯ 208 unchanged lines | |||
modifiedCargo.toml+5 −2
| ⋯ 30 unchanged lines | |||
| 31 | 31 | maud = { version = "0.27", features = ["axum"] } | |
| 32 | 32 | rand = "0.10" | |
| 33 | 33 | serde = { version = "1", features = ["derive"] } | |
| 34 | + | serde_yaml = "0.9" | |
| 34 | 35 | similar = "2" | |
| 35 | 36 | syntect = { version = "5", default-features = false, features = ["default-fancy"] } | |
| 36 | 37 | thiserror = "2" | |
| ⋯ 7 unchanged lines | |||
| 44 | 45 | tracing = "0.1" | |
| 45 | 46 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } | |
| 46 | 47 | ||
| 47 | - | # ssh | |
| 48 | - | russh = "0.61" | |
| 48 | + | # ssh — use the `ring` crypto backend instead of the default `aws-lc-rs`: | |
| 49 | + | # ring is far cheaper to compile (no cmake/perl) and cross-compiles cleanly | |
| 50 | + | # (zigbuild/musl), which matters for building images for the low-RAM VPS. | |
| 51 | + | russh = { version = "0.61", default-features = false, features = ["flate2", "ring", "rsa"] } | |
| 49 | 52 | # ssh-key parsing/fingerprinting, shared by anvil-core (storage) and anvil-ssh | |
| 50 | 53 | # (auth). Pinned to match russh's transitive ssh-key so fingerprints agree. | |
| 51 | 54 | ssh-key = "0.7.0-rc.10" | |
modifiedcrates/anvil-core/Cargo.toml+1 −0
| ⋯ 12 unchanged lines | |||
| 13 | 13 | argon2.workspace = true | |
| 14 | 14 | ssh-key.workspace = true | |
| 15 | 15 | serde.workspace = true | |
| 16 | + | serde_yaml.workspace = true | |
| 16 | 17 | toml.workspace = true | |
| 17 | 18 | thiserror.workspace = true | |
| 18 | 19 | tracing.workspace = true | |
| ⋯ 4 unchanged lines | |||
addedcrates/anvil-core/src/ci.rs+209 −0
| 1 | + | //! Continuous integration: the pipeline definition (`.anvil/ci.toml`) and the | |
| 2 | + | //! persistence/lifecycle of CI runs. Execution (Docker) lives in `anvil-ci`. | |
| 3 | + | ||
| 4 | + | use serde::Deserialize; | |
| 5 | + | ||
| 6 | + | use crate::error::{Error, Result}; | |
| 7 | + | use crate::models::CiRun; | |
| 8 | + | ||
| 9 | + | /// Run status values stored in [`CiRun::status`]. | |
| 10 | + | pub mod status { | |
| 11 | + | pub const QUEUED: &str = "queued"; | |
| 12 | + | pub const RUNNING: &str = "running"; | |
| 13 | + | pub const SUCCESS: &str = "success"; | |
| 14 | + | pub const FAILURE: &str = "failure"; | |
| 15 | + | pub const ERROR: &str = "error"; | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | /// Path of the pipeline definition within a repository. | |
| 19 | + | pub const PIPELINE_PATH: &str = ".anvil/ci.yml"; | |
| 20 | + | ||
| 21 | + | /// A parsed pipeline: a base image and ordered straight-line steps. | |
| 22 | + | #[derive(Debug, Clone, Deserialize)] | |
| 23 | + | pub struct Pipeline { | |
| 24 | + | /// Docker image the steps run in, e.g. `rust:1.95-bookworm`. | |
| 25 | + | pub image: String, | |
| 26 | + | #[serde(default)] | |
| 27 | + | pub steps: Vec<Step>, | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | /// One pipeline step: a shell command, with an optional display name. | |
| 31 | + | #[derive(Debug, Clone, Deserialize)] | |
| 32 | + | pub struct Step { | |
| 33 | + | #[serde(default)] | |
| 34 | + | pub name: String, | |
| 35 | + | pub run: String, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | impl Step { | |
| 39 | + | /// Display label: the explicit name, or the command if unnamed. | |
| 40 | + | pub fn label(&self) -> &str { | |
| 41 | + | if self.name.is_empty() { | |
| 42 | + | &self.run | |
| 43 | + | } else { | |
| 44 | + | &self.name | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /// Parse a `.anvil/ci.yml` pipeline definition. | |
| 50 | + | pub fn parse_pipeline(yaml: &str) -> Result<Pipeline> { | |
| 51 | + | let pipeline: Pipeline = serde_yaml::from_str(yaml) | |
| 52 | + | .map_err(|e| Error::Invalid(format!("invalid {PIPELINE_PATH}: {e}")))?; | |
| 53 | + | if pipeline.image.trim().is_empty() { | |
| 54 | + | return Err(Error::Invalid(format!("{PIPELINE_PATH}: `image` is required"))); | |
| 55 | + | } | |
| 56 | + | Ok(pipeline) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | /// Create a queued CI run for a pushed commit. | |
| 60 | + | pub async fn enqueue( | |
| 61 | + | db: &toasty::Db, | |
| 62 | + | repo_id: i64, | |
| 63 | + | commit: &str, | |
| 64 | + | ref_name: &str, | |
| 65 | + | ) -> Result<CiRun> { | |
| 66 | + | let mut conn = db.clone(); | |
| 67 | + | let run = toasty::create!(CiRun { | |
| 68 | + | repo_id: repo_id, | |
| 69 | + | commit: commit, | |
| 70 | + | ref_name: ref_name, | |
| 71 | + | status: status::QUEUED, | |
| 72 | + | log: "", | |
| 73 | + | created_at: crate::now(), | |
| 74 | + | started_at: 0, | |
| 75 | + | finished_at: 0, | |
| 76 | + | }) | |
| 77 | + | .exec(&mut conn) | |
| 78 | + | .await?; | |
| 79 | + | Ok(run) | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | /// Fetch a run by id. | |
| 83 | + | pub async fn get(db: &toasty::Db, id: i64) -> Result<Option<CiRun>> { | |
| 84 | + | let mut conn = db.clone(); | |
| 85 | + | Ok(CiRun::filter(CiRun::fields().id().eq(id)) | |
| 86 | + | .first() | |
| 87 | + | .exec(&mut conn) | |
| 88 | + | .await?) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | /// List a repository's runs, newest first, up to `limit`. | |
| 92 | + | pub async fn list_by_repo(db: &toasty::Db, repo_id: i64, limit: usize) -> Result<Vec<CiRun>> { | |
| 93 | + | let mut conn = db.clone(); | |
| 94 | + | let mut runs = CiRun::filter(CiRun::fields().repo_id().eq(repo_id)) | |
| 95 | + | .exec(&mut conn) | |
| 96 | + | .await?; | |
| 97 | + | runs.sort_by(|a, b| b.id.cmp(&a.id)); | |
| 98 | + | runs.truncate(limit); | |
| 99 | + | Ok(runs) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | /// The most recent run for a specific commit (for status badges). | |
| 103 | + | pub async fn latest_for_commit( | |
| 104 | + | db: &toasty::Db, | |
| 105 | + | repo_id: i64, | |
| 106 | + | commit: &str, | |
| 107 | + | ) -> Result<Option<CiRun>> { | |
| 108 | + | let mut conn = db.clone(); | |
| 109 | + | let mut runs = CiRun::filter(CiRun::fields().repo_id().eq(repo_id)) | |
| 110 | + | .exec(&mut conn) | |
| 111 | + | .await?; | |
| 112 | + | runs.retain(|r| r.commit == commit); | |
| 113 | + | runs.sort_by(|a, b| b.id.cmp(&a.id)); | |
| 114 | + | Ok(runs.into_iter().next()) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | /// Mark a run as started (running). | |
| 118 | + | pub async fn mark_running(db: &toasty::Db, id: i64) -> Result<()> { | |
| 119 | + | let Some(mut run) = get(db, id).await? else { | |
| 120 | + | return Ok(()); | |
| 121 | + | }; | |
| 122 | + | let mut conn = db.clone(); | |
| 123 | + | run.update() | |
| 124 | + | .status(status::RUNNING) | |
| 125 | + | .started_at(crate::now()) | |
| 126 | + | .exec(&mut conn) | |
| 127 | + | .await?; | |
| 128 | + | Ok(()) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | /// Append a chunk to a run's log. | |
| 132 | + | pub async fn append_log(db: &toasty::Db, id: i64, chunk: &str) -> Result<()> { | |
| 133 | + | let Some(mut run) = get(db, id).await? else { | |
| 134 | + | return Ok(()); | |
| 135 | + | }; | |
| 136 | + | let combined = format!("{}{chunk}", run.log); | |
| 137 | + | let mut conn = db.clone(); | |
| 138 | + | run.update().log(&combined).exec(&mut conn).await?; | |
| 139 | + | Ok(()) | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | /// Finish a run with a terminal status (`success`/`failure`/`error`). | |
| 143 | + | pub async fn finish(db: &toasty::Db, id: i64, status: &str) -> Result<()> { | |
| 144 | + | let Some(mut run) = get(db, id).await? else { | |
| 145 | + | return Ok(()); | |
| 146 | + | }; | |
| 147 | + | let mut conn = db.clone(); | |
| 148 | + | run.update() | |
| 149 | + | .status(status) | |
| 150 | + | .finished_at(crate::now()) | |
| 151 | + | .exec(&mut conn) | |
| 152 | + | .await?; | |
| 153 | + | Ok(()) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | /// Re-queue runs left mid-flight by a crash/restart (status `running`). | |
| 157 | + | /// Returns the ids that were requeued so the runner can pick them up. | |
| 158 | + | pub async fn requeue_interrupted(db: &toasty::Db) -> Result<Vec<i64>> { | |
| 159 | + | let mut conn = db.clone(); | |
| 160 | + | let interrupted = CiRun::filter(CiRun::fields().status().eq(status::RUNNING)) | |
| 161 | + | .exec(&mut conn) | |
| 162 | + | .await?; | |
| 163 | + | let mut ids = Vec::new(); | |
| 164 | + | for mut run in interrupted { | |
| 165 | + | let mut conn = db.clone(); | |
| 166 | + | run.update().status(status::QUEUED).exec(&mut conn).await?; | |
| 167 | + | ids.push(run.id); | |
| 168 | + | } | |
| 169 | + | Ok(ids) | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | /// List all queued run ids (oldest first) — used on startup to drain the queue. | |
| 173 | + | pub async fn queued_ids(db: &toasty::Db) -> Result<Vec<i64>> { | |
| 174 | + | let mut conn = db.clone(); | |
| 175 | + | let mut runs = CiRun::filter(CiRun::fields().status().eq(status::QUEUED)) | |
| 176 | + | .exec(&mut conn) | |
| 177 | + | .await?; | |
| 178 | + | runs.sort_by(|a, b| a.id.cmp(&b.id)); | |
| 179 | + | Ok(runs.into_iter().map(|r| r.id).collect()) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | #[cfg(test)] | |
| 183 | + | mod tests { | |
| 184 | + | use super::*; | |
| 185 | + | ||
| 186 | + | #[test] | |
| 187 | + | fn parses_a_basic_pipeline() { | |
| 188 | + | // YAML is indentation-sensitive, so the fixture is flush-left. | |
| 189 | + | let p = parse_pipeline( | |
| 190 | + | r#"image: rust:1.95-bookworm | |
| 191 | + | steps: | |
| 192 | + | - name: test | |
| 193 | + | run: cargo test --workspace | |
| 194 | + | - run: cargo build --release | |
| 195 | + | "#, | |
| 196 | + | ) | |
| 197 | + | .unwrap(); | |
| 198 | + | assert_eq!(p.image, "rust:1.95-bookworm"); | |
| 199 | + | assert_eq!(p.steps.len(), 2); | |
| 200 | + | assert_eq!(p.steps[0].label(), "test"); | |
| 201 | + | // Unnamed step falls back to its command for the label. | |
| 202 | + | assert_eq!(p.steps[1].label(), "cargo build --release"); | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | #[test] | |
| 206 | + | fn requires_an_image() { | |
| 207 | + | assert!(parse_pipeline("steps: []\n").is_err()); | |
| 208 | + | } | |
| 209 | + | } |
modifiedcrates/anvil-core/src/db.rs+2 −1
| ⋯ 3 unchanged lines | |||
| 4 | 4 | ||
| 5 | 5 | use crate::error::Result; | |
| 6 | 6 | use crate::models::{ | |
| 7 | + | CiRun, | |
| 7 | 8 | Repository, | |
| 8 | 9 | Session, | |
| 9 | 10 | SshKey, | |
| ⋯ 15 unchanged lines | |||
| 25 | 26 | let url = format!("sqlite:{}", path.display()); | |
| 26 | 27 | ||
| 27 | 28 | let db = toasty::Db::builder() | |
| 28 | - | .models(toasty::models!(User, Repository, SshKey, Session)) | |
| 29 | + | .models(toasty::models!(User, Repository, SshKey, Session, CiRun)) | |
| 29 | 30 | .connect(&url) | |
| 30 | 31 | .await?; | |
| 31 | 32 | ||
| ⋯ 5 unchanged lines | |||
modifiedcrates/anvil-core/src/lib.rs+2 −0
| ⋯ 5 unchanged lines | |||
| 6 | 6 | //! `anvil-git`) build on top of it. | |
| 7 | 7 | ||
| 8 | 8 | pub mod access; | |
| 9 | + | pub mod ci; | |
| 9 | 10 | pub mod config; | |
| 10 | 11 | pub mod db; | |
| 11 | 12 | pub mod error; | |
| ⋯ 10 unchanged lines | |||
| 22 | 23 | Result, | |
| 23 | 24 | }; | |
| 24 | 25 | pub use models::{ | |
| 26 | + | CiRun, | |
| 25 | 27 | Repository, | |
| 26 | 28 | Session, | |
| 27 | 29 | SshKey, | |
| ⋯ 33 unchanged lines | |||
modifiedcrates/anvil-core/src/models.rs+24 −0
| ⋯ 36 unchanged lines | |||
| 37 | 37 | pub created_at: i64, | |
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | + | /// A CI run for a pushed commit. | |
| 41 | + | /// | |
| 42 | + | /// `status` is one of `queued`, `running`, `success`, `failure` (a step exited | |
| 43 | + | /// non-zero), or `error` (the runner itself failed). `started_at`/`finished_at` | |
| 44 | + | /// are 0 until they occur. | |
| 45 | + | #[derive(Debug, Clone, toasty::Model)] | |
| 46 | + | pub struct CiRun { | |
| 47 | + | #[key] | |
| 48 | + | #[auto] | |
| 49 | + | pub id: i64, | |
| 50 | + | #[index] | |
| 51 | + | pub repo_id: i64, | |
| 52 | + | /// Full commit SHA the run is for. | |
| 53 | + | pub commit: String, | |
| 54 | + | /// Short branch name that was pushed (e.g. `main`). | |
| 55 | + | pub ref_name: String, | |
| 56 | + | pub status: String, | |
| 57 | + | /// Accumulated run log. | |
| 58 | + | pub log: String, | |
| 59 | + | pub created_at: i64, | |
| 60 | + | pub started_at: i64, | |
| 61 | + | pub finished_at: i64, | |
| 62 | + | } | |
| 63 | + | ||
| 40 | 64 | /// A web login session, keyed by an opaque random token stored in a cookie. | |
| 41 | 65 | #[derive(Debug, toasty::Model)] | |
| 42 | 66 | pub struct Session { | |
| ⋯ 25 unchanged lines | |||
modifiedcrates/anvil-git/Cargo.toml+1 −0
| ⋯ 10 unchanged lines | |||
| 11 | 11 | anvil-core.workspace = true | |
| 12 | 12 | gix.workspace = true | |
| 13 | 13 | gitserver-core.workspace = true | |
| 14 | + | toasty.workspace = true | |
| 14 | 15 | tokio.workspace = true | |
| 15 | 16 | thiserror.workspace = true | |
| 16 | 17 | tracing.workspace = true | |
modifiedcrates/anvil-git/src/lib.rs+1 −0
| ⋯ 16 unchanged lines | |||
| 17 | 17 | pub mod error; | |
| 18 | 18 | pub mod smart_http; | |
| 19 | 19 | pub mod ssh; | |
| 20 | + | pub mod trigger; | |
| 20 | 21 | ||
| 21 | 22 | pub use error::{ | |
| 22 | 23 | Error, | |
| ⋯ 6 unchanged lines | |||
addedcrates/anvil-git/src/trigger.rs+66 −0
| 1 | + | //! Post-push CI trigger: detect which branches changed on a push and enqueue a | |
| 2 | + | //! CI run for any whose new commit defines a pipeline (`.anvil/ci.yml`). | |
| 3 | + | //! | |
| 4 | + | //! Used by the receive-pack paths in `anvil-web` and `anvil-ssh`: snapshot the | |
| 5 | + | //! branch tips *before* the push, then call [`enqueue_ci_for_push`] after it | |
| 6 | + | //! succeeds. | |
| 7 | + | ||
| 8 | + | use std::collections::HashMap; | |
| 9 | + | use std::path::Path; | |
| 10 | + | ||
| 11 | + | /// Local branch short-name → tip commit id (hex). | |
| 12 | + | pub type BranchTips = HashMap<String, String>; | |
| 13 | + | ||
| 14 | + | /// Snapshot current local branch tips. Best-effort — returns empty on error. | |
| 15 | + | pub fn snapshot_branches(repo_path: &Path) -> BranchTips { | |
| 16 | + | let mut tips = HashMap::new(); | |
| 17 | + | let Ok(repo) = gix::open(repo_path) else { | |
| 18 | + | return tips; | |
| 19 | + | }; | |
| 20 | + | let Ok(platform) = repo.references() else { | |
| 21 | + | return tips; | |
| 22 | + | }; | |
| 23 | + | let Ok(iter) = platform.local_branches() else { | |
| 24 | + | return tips; | |
| 25 | + | }; | |
| 26 | + | for mut branch in iter.flatten() { | |
| 27 | + | let name = branch.name().shorten().to_string(); | |
| 28 | + | if let Ok(id) = branch.peel_to_id() { | |
| 29 | + | tips.insert(name, id.detach().to_string()); | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | tips | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | /// After a push, enqueue a CI run for each branch whose tip changed (or is new) | |
| 36 | + | /// and whose new commit contains a pipeline definition. Returns the enqueued | |
| 37 | + | /// run ids (so the caller can notify the runner). | |
| 38 | + | pub async fn enqueue_ci_for_push( | |
| 39 | + | db: &toasty::Db, | |
| 40 | + | repo_id: i64, | |
| 41 | + | repo_path: &Path, | |
| 42 | + | before: &BranchTips, | |
| 43 | + | ) -> Vec<i64> { | |
| 44 | + | let after = snapshot_branches(repo_path); | |
| 45 | + | let mut enqueued = Vec::new(); | |
| 46 | + | for (branch, new_tip) in &after { | |
| 47 | + | if before.get(branch).map(String::as_str) == Some(new_tip.as_str()) { | |
| 48 | + | continue; // unchanged branch | |
| 49 | + | } | |
| 50 | + | let has_pipeline = matches!( | |
| 51 | + | crate::browse::read_blob(repo_path, new_tip, anvil_core::ci::PIPELINE_PATH), | |
| 52 | + | Ok(Some(_)) | |
| 53 | + | ); | |
| 54 | + | if !has_pipeline { | |
| 55 | + | continue; | |
| 56 | + | } | |
| 57 | + | match anvil_core::ci::enqueue(db, repo_id, new_tip, branch).await { | |
| 58 | + | Ok(run) => { | |
| 59 | + | tracing::info!("enqueued CI run {} for {branch}@{new_tip}", run.id); | |
| 60 | + | enqueued.push(run.id); | |
| 61 | + | } | |
| 62 | + | Err(e) => tracing::error!("failed to enqueue CI run: {e}"), | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | enqueued | |
| 66 | + | } |
modifiedcrates/anvil-ssh/src/lib.rs+18 −4
| ⋯ 180 unchanged lines | |||
| 181 | 181 | return fail(session, channel_id, "unsupported command"); | |
| 182 | 182 | }; | |
| 183 | 183 | let need_write = service == anvil_git::Service::ReceivePack; | |
| 184 | - | let path = match authorize_repo(&self.app, self.authed_user, &rel, need_write).await { | |
| 185 | - | Ok(path) => path, | |
| 184 | + | let (path, repo_id) = match authorize_repo(&self.app, self.authed_user, &rel, need_write).await | |
| 185 | + | { | |
| 186 | + | Ok(resolved) => resolved, | |
| 186 | 187 | Err(message) => return fail(session, channel_id, message), | |
| 187 | 188 | }; | |
| 188 | 189 | let Some(channel) = self.channel.take() else { | |
| ⋯ 8 unchanged lines | |||
| 197 | 198 | ||
| 198 | 199 | let protocol_v2 = self.protocol_v2; | |
| 199 | 200 | let handle = session.handle(); | |
| 201 | + | let db = self.app.db.clone(); | |
| 200 | 202 | session.channel_success(channel_id)?; | |
| 201 | 203 | ||
| 202 | 204 | tokio::spawn(async move { | |
| 205 | + | // For a push, snapshot branch tips before serving so we can detect | |
| 206 | + | // what changed and trigger CI afterward. | |
| 207 | + | let before = (service == anvil_git::Service::ReceivePack) | |
| 208 | + | .then(|| anvil_git::trigger::snapshot_branches(&path)); | |
| 209 | + | ||
| 203 | 210 | let stream = channel.into_stream(); | |
| 204 | 211 | let code = match git_ssh::serve(&path, service, protocol_v2, stream).await { | |
| 205 | 212 | Ok(()) => 0, | |
| ⋯ 2 unchanged lines | |||
| 208 | 215 | 1 | |
| 209 | 216 | } | |
| 210 | 217 | }; | |
| 218 | + | ||
| 219 | + | if code == 0 { | |
| 220 | + | if let Some(before) = before { | |
| 221 | + | anvil_git::trigger::enqueue_ci_for_push(&db, repo_id, &path, &before).await; | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | ||
| 211 | 225 | let _ = handle.exit_status_request(channel_id, code).await; | |
| 212 | 226 | let _ = handle.eof(channel_id).await; | |
| 213 | 227 | let _ = handle.close(channel_id).await; | |
| ⋯ 18 unchanged lines | |||
| 232 | 246 | authed_user: Option<i64>, | |
| 233 | 247 | rel: &str, | |
| 234 | 248 | need_write: bool, | |
| 235 | - | ) -> Result<PathBuf, &'static str> { | |
| 249 | + | ) -> Result<(PathBuf, i64), &'static str> { | |
| 236 | 250 | let (owner, repo) = rel | |
| 237 | 251 | .trim_start_matches('/') | |
| 238 | 252 | .split_once('/') | |
| ⋯ 32 unchanged lines | |||
| 271 | 285 | if !path.exists() { | |
| 272 | 286 | return Err("repository not found"); | |
| 273 | 287 | } | |
| 274 | - | Ok(path) | |
| 288 | + | Ok((path, repo.id)) | |
| 275 | 289 | } | |
modifiedcrates/anvil-web/src/git_http.rs+6 −1
| ⋯ 212 unchanged lines | |||
| 213 | 213 | return resp; | |
| 214 | 214 | } | |
| 215 | 215 | ||
| 216 | + | // Snapshot branch tips before the push so we can detect what changed. | |
| 217 | + | let before = anvil_git::trigger::snapshot_branches(&path); | |
| 216 | 218 | let reader = std::io::Cursor::new(body.to_vec()); | |
| 217 | 219 | match smart_http::receive_pack(&path, reader).await { | |
| 218 | - | Ok(b) => rpc_response(Service::ReceivePack.result_content_type(), Body::from(b)), | |
| 220 | + | Ok(b) => { | |
| 221 | + | anvil_git::trigger::enqueue_ci_for_push(&app.db, meta.id, &path, &before).await; | |
| 222 | + | rpc_response(Service::ReceivePack.result_content_type(), Body::from(b)) | |
| 223 | + | } | |
| 219 | 224 | Err(e) => internal(e), | |
| 220 | 225 | } | |
| 221 | 226 | } | |