@ebrindley/build 0.0.0-stage → 1.42.1-rc28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-project/providers/catalog.json +13951 -0
- package/.build-project/providers/pricing.json +1978 -0
- package/.build-project/providers/registry.json +4103 -0
- package/.github/SECURITY.md +75 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +661 -0
- package/CODE_OF_CONDUCT.md +44 -0
- package/CONTRIBUTING.md +24 -0
- package/INSTALL.md +623 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/THIRD_PARTY_NOTICES.txt +26 -0
- package/dist/ai-judge-UE5UOYOO.js +191 -0
- package/dist/allowlist-grounding-KPLN6NC5.js +16 -0
- package/dist/allowlist-utils-MZZKKXRD.js +31 -0
- package/dist/apply-completion-mode-EM37UZHB.js +110 -0
- package/dist/artifact-migrator-WPACNSEP.js +276 -0
- package/dist/ask-2OZ4JFQR.js +176 -0
- package/dist/auth-6UKBTJUQ.js +185 -0
- package/dist/auth-H5KJL6HL.js +165 -0
- package/dist/auth-LVJRJDMU.js +12 -0
- package/dist/auth-NHZOCNRR.js +94 -0
- package/dist/auth-PBXHWXGW.js +95 -0
- package/dist/auth-T7Y5DCAH.js +91 -0
- package/dist/auth-TVVKCHQW.js +13 -0
- package/dist/auth-UHB2UCSS.js +97 -0
- package/dist/auth-VACA3VUL.js +98 -0
- package/dist/auth-VVLC77S3.js +44 -0
- package/dist/auth-liveness-XO4AGDDD.js +53 -0
- package/dist/autoloop-cleanup-SI3VGAAR.js +106 -0
- package/dist/autoloop-evidence-gates-J5GG5YQB.js +39 -0
- package/dist/autoloop-gate-policy-SUNAEP6Q.js +169 -0
- package/dist/autoloop-helpers-VQGQNTT6.js +52 -0
- package/dist/autoloop-ledger-P6TIDP7C.js +96 -0
- package/dist/autoloop-ledger-subscriber-HFAGOZXH.js +161 -0
- package/dist/autoloop-run-defaults-GWGZBNT7.js +27 -0
- package/dist/autoloop-run-lock-GBWRM5A2.js +197 -0
- package/dist/autoloop-success-C32QVWOP.js +93 -0
- package/dist/autoloop-termination-summary-GI6IBQWD.js +58 -0
- package/dist/autoloop-trajectory-AMKSQISD.js +70 -0
- package/dist/autoloop-wall-clock-T7ZVIQOC.js +12 -0
- package/dist/automatic-branch-cleanup-R62X2C5O.js +49 -0
- package/dist/autonomous-loop-controller-2WL363LW.js +4386 -0
- package/dist/backend-YUTFDZXZ.js +30 -0
- package/dist/background-executor-Z3ODTMD7.js +260 -0
- package/dist/backlog-75QPEAMW.js +403 -0
- package/dist/backlog-execution-intent-GWUV5TUK.js +19 -0
- package/dist/backup-active-store-XHWLN34M.js +16 -0
- package/dist/backup-machine-stores-UI4LTJOY.js +251 -0
- package/dist/backup-telemetry-db-Z4ICYCNW.js +26 -0
- package/dist/branch-archive-manager-FMSQ7YJF.js +18 -0
- package/dist/branch-cleanup-manager-ST6FJGND.js +35 -0
- package/dist/build-gate-7NKWPY62.js +68 -0
- package/dist/build-root-S7DINODY.js +18 -0
- package/dist/change-summary-R6CB6VXJ.js +50 -0
- package/dist/chunk-24MNKKOM.js +173 -0
- package/dist/chunk-2H6VZFIO.js +235 -0
- package/dist/chunk-2JIX3EKV.js +39 -0
- package/dist/chunk-2KO7HVZM.js +106 -0
- package/dist/chunk-2L36U25O.js +100 -0
- package/dist/chunk-2NMNFUQA.js +475 -0
- package/dist/chunk-2OUAEH67.js +83 -0
- package/dist/chunk-2VLJUXJU.js +115 -0
- package/dist/chunk-2VUEDIUM.js +77 -0
- package/dist/chunk-2Y2IIHPI.js +430 -0
- package/dist/chunk-2YRG3LHZ.js +220 -0
- package/dist/chunk-2Z5Q3SUY.js +65 -0
- package/dist/chunk-32IYXIE5.js +873 -0
- package/dist/chunk-33IGPPUI.js +292 -0
- package/dist/chunk-352PCJLK.js +283 -0
- package/dist/chunk-35J4NE6S.js +226 -0
- package/dist/chunk-3C7WBIVK.js +1016 -0
- package/dist/chunk-3H3EY6U7.js +1225 -0
- package/dist/chunk-3H4MWYIC.js +198 -0
- package/dist/chunk-3HHWQR3N.js +2479 -0
- package/dist/chunk-3JKNRYEE.js +14 -0
- package/dist/chunk-3LECCYZY.js +128 -0
- package/dist/chunk-3LOY6YZI.js +852 -0
- package/dist/chunk-3M57DADF.js +44 -0
- package/dist/chunk-3NOTLVLG.js +3803 -0
- package/dist/chunk-3OPP2OZB.js +1480 -0
- package/dist/chunk-3P5IN5E7.js +9 -0
- package/dist/chunk-3PL22IYZ.js +20 -0
- package/dist/chunk-3REDAJUJ.js +14 -0
- package/dist/chunk-3S2R4XJI.js +203 -0
- package/dist/chunk-3UBZI5DW.js +61 -0
- package/dist/chunk-3VV5OBGG.js +308 -0
- package/dist/chunk-3WY5TU2O.js +823 -0
- package/dist/chunk-43OKHTW4.js +82 -0
- package/dist/chunk-45KUJBDI.js +112 -0
- package/dist/chunk-47X5LM7B.js +224 -0
- package/dist/chunk-4B6FQJ77.js +268 -0
- package/dist/chunk-4BTDUT2S.js +42 -0
- package/dist/chunk-4DJL2VXA.js +24 -0
- package/dist/chunk-4EBSXZ73.js +48 -0
- package/dist/chunk-4QKP6I4C.js +294 -0
- package/dist/chunk-4U6XK2DG.js +300 -0
- package/dist/chunk-4UMRUFBD.js +132 -0
- package/dist/chunk-4US4SYJ4.js +569 -0
- package/dist/chunk-4ZPLWEZO.js +154 -0
- package/dist/chunk-56WPYDJH.js +624 -0
- package/dist/chunk-57WRCJJW.js +50 -0
- package/dist/chunk-5AW4RMNG.js +24 -0
- package/dist/chunk-5EQN56OZ.js +155 -0
- package/dist/chunk-5F43TUIN.js +38 -0
- package/dist/chunk-5FKHIKTS.js +115 -0
- package/dist/chunk-5HDP7XZG.js +91 -0
- package/dist/chunk-5KQETTNS.js +152 -0
- package/dist/chunk-5PDVN2KO.js +498 -0
- package/dist/chunk-5UBIFVRD.js +509 -0
- package/dist/chunk-5YDE3Q62.js +46 -0
- package/dist/chunk-626AJVMU.js +302 -0
- package/dist/chunk-633XZ7UF.js +3583 -0
- package/dist/chunk-63DHNHGA.js +236 -0
- package/dist/chunk-666656AR.js +568 -0
- package/dist/chunk-67YRJPJO.js +19 -0
- package/dist/chunk-6CAU7VBA.js +20 -0
- package/dist/chunk-6CB3KJRY.js +21 -0
- package/dist/chunk-6DBMALXL.js +29 -0
- package/dist/chunk-6E25C4IA.js +269 -0
- package/dist/chunk-6FIBNIQU.js +73 -0
- package/dist/chunk-6G57FYBV.js +189 -0
- package/dist/chunk-6KZKRTG3.js +4885 -0
- package/dist/chunk-6LAH2UV2.js +525 -0
- package/dist/chunk-6PZY2TTU.js +921 -0
- package/dist/chunk-6T6NHXK4.js +230 -0
- package/dist/chunk-6TBEX4R4.js +432 -0
- package/dist/chunk-6TRD7TEE.js +911 -0
- package/dist/chunk-6XCP2RAI.js +47 -0
- package/dist/chunk-726G6WDL.js +351 -0
- package/dist/chunk-75EMT7BF.js +616 -0
- package/dist/chunk-762ACDZJ.js +4696 -0
- package/dist/chunk-77I6G4CO.js +1 -0
- package/dist/chunk-7DJ6SQH3.js +510 -0
- package/dist/chunk-7HPCIOMA.js +1383 -0
- package/dist/chunk-7HYBSRPZ.js +17503 -0
- package/dist/chunk-7KBEOPVB.js +73 -0
- package/dist/chunk-7LHJDJB3.js +2222 -0
- package/dist/chunk-7NVJMH52.js +306 -0
- package/dist/chunk-7RMARDVH.js +33 -0
- package/dist/chunk-7SEWU4QG.js +36 -0
- package/dist/chunk-7TUTZTI2.js +911 -0
- package/dist/chunk-7VBDFPUM.js +340 -0
- package/dist/chunk-7VRJHTGZ.js +6406 -0
- package/dist/chunk-7YAOUUKK.js +493 -0
- package/dist/chunk-AAAODV76.js +860 -0
- package/dist/chunk-AB4HUJVQ.js +339 -0
- package/dist/chunk-AEQWV746.js +611 -0
- package/dist/chunk-AHU3FKPA.js +11639 -0
- package/dist/chunk-AISTV5XU.js +226 -0
- package/dist/chunk-AJBTKGJZ.js +48 -0
- package/dist/chunk-AJJNW7XT.js +248 -0
- package/dist/chunk-AJN5Y5YZ.js +160 -0
- package/dist/chunk-ALDTFNZ3.js +150 -0
- package/dist/chunk-ARARVIZK.js +281 -0
- package/dist/chunk-ATBM6AW5.js +53 -0
- package/dist/chunk-ATKEKM3E.js +206 -0
- package/dist/chunk-AUJLW6JZ.js +92 -0
- package/dist/chunk-AY3MHEFM.js +24 -0
- package/dist/chunk-B7PYTV72.js +236 -0
- package/dist/chunk-B7YRSULE.js +1423 -0
- package/dist/chunk-BAWJNMWF.js +339 -0
- package/dist/chunk-BBPIILO2.js +88 -0
- package/dist/chunk-BD3PGW6A.js +957 -0
- package/dist/chunk-BDPYFAB4.js +44 -0
- package/dist/chunk-BIUV23HH.js +729 -0
- package/dist/chunk-BQ3VAGXH.js +243 -0
- package/dist/chunk-BQJOGAWT.js +31 -0
- package/dist/chunk-BT7EG7CO.js +68 -0
- package/dist/chunk-BV3XQQLU.js +27 -0
- package/dist/chunk-BY6ODHNZ.js +78 -0
- package/dist/chunk-BZRLSZNV.js +79 -0
- package/dist/chunk-C3DH3NCY.js +2773 -0
- package/dist/chunk-C3KCMAJP.js +756 -0
- package/dist/chunk-C7AGUJ5K.js +267 -0
- package/dist/chunk-CBSWKQCX.js +342 -0
- package/dist/chunk-CE5OZBYY.js +21 -0
- package/dist/chunk-CFCVYHSW.js +373 -0
- package/dist/chunk-CHA2HDE5.js +126 -0
- package/dist/chunk-CIHWPGS5.js +146 -0
- package/dist/chunk-CJBSZX3F.js +127 -0
- package/dist/chunk-CLA6QIJM.js +43 -0
- package/dist/chunk-CPSVQR2I.js +365 -0
- package/dist/chunk-CRPRVNQ4.js +254 -0
- package/dist/chunk-CSIE4Y7C.js +1093 -0
- package/dist/chunk-CSOPSYDY.js +142 -0
- package/dist/chunk-CSXLLEGB.js +205 -0
- package/dist/chunk-CTDLERWU.js +293 -0
- package/dist/chunk-CULBXMSK.js +110 -0
- package/dist/chunk-D2M2Y5ID.js +21 -0
- package/dist/chunk-D37WY7SH.js +1060 -0
- package/dist/chunk-D4VJFCGZ.js +89 -0
- package/dist/chunk-D6WTZMZB.js +48 -0
- package/dist/chunk-DBHOWDUH.js +976 -0
- package/dist/chunk-DCACX7BV.js +19364 -0
- package/dist/chunk-DFHX4XYA.js +128 -0
- package/dist/chunk-DIKBRRKE.js +176 -0
- package/dist/chunk-DJG5RIOR.js +9 -0
- package/dist/chunk-DNIOIISW.js +616 -0
- package/dist/chunk-DOA5G7IA.js +315 -0
- package/dist/chunk-DP7FKP6W.js +855 -0
- package/dist/chunk-DS2JZ677.js +79 -0
- package/dist/chunk-DSRLVYBS.js +82 -0
- package/dist/chunk-EAEVKNJA.js +209 -0
- package/dist/chunk-EEUESN3T.js +575 -0
- package/dist/chunk-EJ4LUU5Y.js +73 -0
- package/dist/chunk-EJIWTPSZ.js +1063 -0
- package/dist/chunk-EJVCDTAN.js +116 -0
- package/dist/chunk-EKLUBDBQ.js +627 -0
- package/dist/chunk-EN62WLFD.js +849 -0
- package/dist/chunk-ENYRCL43.js +26 -0
- package/dist/chunk-EP22ZMFO.js +273 -0
- package/dist/chunk-EP4SVAVI.js +87 -0
- package/dist/chunk-EPAMVGI3.js +1562 -0
- package/dist/chunk-EPRMH6HO.js +24 -0
- package/dist/chunk-ES6NWJ36.js +4057 -0
- package/dist/chunk-ETA2K4KI.js +30 -0
- package/dist/chunk-ETAD4NKD.js +31 -0
- package/dist/chunk-EUWIIDQ7.js +93 -0
- package/dist/chunk-EVE3OD2W.js +141 -0
- package/dist/chunk-EXA26FFC.js +459 -0
- package/dist/chunk-EXSPAA35.js +244 -0
- package/dist/chunk-F55YABKV.js +258 -0
- package/dist/chunk-F5DNGU5W.js +27 -0
- package/dist/chunk-F622MJYV.js +103 -0
- package/dist/chunk-F6NIQD74.js +5111 -0
- package/dist/chunk-FAWJTC34.js +43 -0
- package/dist/chunk-FAZBRHGG.js +47 -0
- package/dist/chunk-FIEUSGNJ.js +39 -0
- package/dist/chunk-FKCG7K6H.js +1146 -0
- package/dist/chunk-FOVVXV45.js +229 -0
- package/dist/chunk-FRBPKBFE.js +30 -0
- package/dist/chunk-FSA5BPFI.js +268 -0
- package/dist/chunk-FSJ5TTZ3.js +29 -0
- package/dist/chunk-FT7SGLXT.js +193 -0
- package/dist/chunk-FUUVZZUB.js +62 -0
- package/dist/chunk-FYMRVUNZ.js +1697 -0
- package/dist/chunk-FYTAA5FG.js +162 -0
- package/dist/chunk-G3VB2UCZ.js +3892 -0
- package/dist/chunk-G6OIMVXH.js +405 -0
- package/dist/chunk-G7PQCXVH.js +228 -0
- package/dist/chunk-GAB6UW52.js +1087 -0
- package/dist/chunk-GAFJEYLK.js +29 -0
- package/dist/chunk-GC4CRMTS.js +642 -0
- package/dist/chunk-GFHSIWHL.js +5297 -0
- package/dist/chunk-GI5M67YA.js +735 -0
- package/dist/chunk-GIJWHUWF.js +191 -0
- package/dist/chunk-GO6DRECI.js +1 -0
- package/dist/chunk-GOFVBTMN.js +314 -0
- package/dist/chunk-GQIP72ZJ.js +13 -0
- package/dist/chunk-GQXLXRFA.js +28 -0
- package/dist/chunk-GT6HFKJB.js +48 -0
- package/dist/chunk-GVJGSSKM.js +174 -0
- package/dist/chunk-GWHGTG37.js +36 -0
- package/dist/chunk-GZAJHLKO.js +18 -0
- package/dist/chunk-GZK62I3N.js +314 -0
- package/dist/chunk-H3HGMISG.js +483 -0
- package/dist/chunk-H3ISZJMJ.js +403 -0
- package/dist/chunk-H4HXVWEC.js +9 -0
- package/dist/chunk-H6MJGKAW.js +120 -0
- package/dist/chunk-HBTTGVR5.js +353 -0
- package/dist/chunk-HBZ322WZ.js +35 -0
- package/dist/chunk-HEP3ILP4.js +34 -0
- package/dist/chunk-HGFXODKO.js +106 -0
- package/dist/chunk-HHWDTB6S.js +59 -0
- package/dist/chunk-HKO45NLZ.js +286 -0
- package/dist/chunk-HLVTEA6A.js +89 -0
- package/dist/chunk-HS3SY6HN.js +160 -0
- package/dist/chunk-HT5OCGKN.js +899 -0
- package/dist/chunk-HXIIEMGF.js +78 -0
- package/dist/chunk-I56TH7VM.js +46 -0
- package/dist/chunk-I6FN3R5D.js +352 -0
- package/dist/chunk-I7SCBHJN.js +655 -0
- package/dist/chunk-IJM4GKAT.js +529 -0
- package/dist/chunk-IMEYU36R.js +344 -0
- package/dist/chunk-INJ3SS6A.js +936 -0
- package/dist/chunk-IUKOPCCE.js +441 -0
- package/dist/chunk-IVEPZQJO.js +33 -0
- package/dist/chunk-IVZEONFC.js +43 -0
- package/dist/chunk-IWBHRSG7.js +936 -0
- package/dist/chunk-IYY3HQAD.js +484 -0
- package/dist/chunk-J2ICMYDA.js +48 -0
- package/dist/chunk-J2QCFLUN.js +505 -0
- package/dist/chunk-J5HM6V5P.js +1039 -0
- package/dist/chunk-J6NZX6QH.js +398 -0
- package/dist/chunk-JBBB2Y47.js +432 -0
- package/dist/chunk-JDCZQZU7.js +42 -0
- package/dist/chunk-JEDNXGSS.js +169 -0
- package/dist/chunk-JHP7OT6K.js +89 -0
- package/dist/chunk-JLS65NUQ.js +14 -0
- package/dist/chunk-JNMWYMC6.js +38 -0
- package/dist/chunk-JNSTWNNW.js +25 -0
- package/dist/chunk-JOSK6GEU.js +9564 -0
- package/dist/chunk-JQUMSIGC.js +100 -0
- package/dist/chunk-JVHGMURQ.js +1106 -0
- package/dist/chunk-JYXS47FQ.js +951 -0
- package/dist/chunk-K2ZMW2Z5.js +2147 -0
- package/dist/chunk-K7FAFORM.js +14 -0
- package/dist/chunk-KBJXX3ON.js +333 -0
- package/dist/chunk-KHFKVQWY.js +486 -0
- package/dist/chunk-KHMEI3DQ.js +173 -0
- package/dist/chunk-KHW3UD7B.js +69 -0
- package/dist/chunk-KKQKWAMF.js +365 -0
- package/dist/chunk-KOKXJV2U.js +455 -0
- package/dist/chunk-KQ4NGOJI.js +14 -0
- package/dist/chunk-KQYQOXMS.js +342 -0
- package/dist/chunk-KTMW65UX.js +14 -0
- package/dist/chunk-KUEWV2JH.js +593 -0
- package/dist/chunk-KXYDV3HP.js +1523 -0
- package/dist/chunk-KZ4JMGZ5.js +96 -0
- package/dist/chunk-KZWGBUIO.js +2659 -0
- package/dist/chunk-L2CBG5IX.js +1892 -0
- package/dist/chunk-L2S235CU.js +314 -0
- package/dist/chunk-L32WD57F.js +98 -0
- package/dist/chunk-LDJCKAJP.js +123 -0
- package/dist/chunk-LDWNBEN7.js +541 -0
- package/dist/chunk-LJVH57G2.js +241 -0
- package/dist/chunk-LPQ7NZEN.js +209 -0
- package/dist/chunk-LRGH3CCV.js +148 -0
- package/dist/chunk-LULOGMMM.js +1133 -0
- package/dist/chunk-LWGKN2GY.js +8284 -0
- package/dist/chunk-LX4F24ZH.js +157 -0
- package/dist/chunk-M4BUEXXF.js +839 -0
- package/dist/chunk-M6UHOC4I.js +553 -0
- package/dist/chunk-MCBNWZNO.js +657 -0
- package/dist/chunk-MEMSY6VY.js +223 -0
- package/dist/chunk-MHLEZQQI.js +145 -0
- package/dist/chunk-MJ6WNSIX.js +192 -0
- package/dist/chunk-MKASIKWZ.js +442 -0
- package/dist/chunk-MNOVBYFU.js +60 -0
- package/dist/chunk-MO2BYAAF.js +2896 -0
- package/dist/chunk-MP6P7Y2A.js +180 -0
- package/dist/chunk-MQ4TFJAG.js +3365 -0
- package/dist/chunk-MT2WAIR3.js +131 -0
- package/dist/chunk-MTU7VTNN.js +1122 -0
- package/dist/chunk-MULMXX4V.js +150 -0
- package/dist/chunk-MW2DWWVA.js +147 -0
- package/dist/chunk-MY3ZPUN4.js +151 -0
- package/dist/chunk-MYLH2S4G.js +64 -0
- package/dist/chunk-MZ5WYKNA.js +28 -0
- package/dist/chunk-N3Q2JNPV.js +249 -0
- package/dist/chunk-N5DX4JAK.js +12 -0
- package/dist/chunk-NAINGKHF.js +35 -0
- package/dist/chunk-NEQUQHOX.js +223 -0
- package/dist/chunk-NHNPEZS4.js +1408 -0
- package/dist/chunk-NHTIXJCR.js +17 -0
- package/dist/chunk-NKLPMBNV.js +1154 -0
- package/dist/chunk-NMVZ5YES.js +199 -0
- package/dist/chunk-NRFRY3GE.js +256 -0
- package/dist/chunk-NTD6CSI6.js +1266 -0
- package/dist/chunk-NUO7B3SH.js +318 -0
- package/dist/chunk-NVOHIJKG.js +1319 -0
- package/dist/chunk-NXH3RX7O.js +523 -0
- package/dist/chunk-NXZ3DURF.js +257 -0
- package/dist/chunk-O26L6F7F.js +209 -0
- package/dist/chunk-O5RRIUEP.js +124 -0
- package/dist/chunk-O5X6BKAO.js +10 -0
- package/dist/chunk-O6ZQUVTR.js +31 -0
- package/dist/chunk-O7FE75QZ.js +306 -0
- package/dist/chunk-OC7Z6AJQ.js +81 -0
- package/dist/chunk-OH7IEZ63.js +307 -0
- package/dist/chunk-OI734GRS.js +115 -0
- package/dist/chunk-OJBH62DK.js +684 -0
- package/dist/chunk-OKJLVSTJ.js +247 -0
- package/dist/chunk-OMX5VL43.js +79 -0
- package/dist/chunk-ONHAMBN6.js +12 -0
- package/dist/chunk-ONYPLIVE.js +753 -0
- package/dist/chunk-OSK7QWPD.js +637 -0
- package/dist/chunk-OTXLMJKP.js +438 -0
- package/dist/chunk-OV5PKQMA.js +213 -0
- package/dist/chunk-OXK5HA4B.js +31 -0
- package/dist/chunk-OYDWFQA4.js +272 -0
- package/dist/chunk-OZAUKWHB.js +22 -0
- package/dist/chunk-P5P2EKGH.js +94 -0
- package/dist/chunk-P6SSWAKY.js +29 -0
- package/dist/chunk-P7CQGPLS.js +30 -0
- package/dist/chunk-P7RVCHQV.js +197 -0
- package/dist/chunk-PEBWDYIW.js +726 -0
- package/dist/chunk-PFCXFO7A.js +9193 -0
- package/dist/chunk-PFLDWMX6.js +168 -0
- package/dist/chunk-PHZGLQSK.js +15883 -0
- package/dist/chunk-PNFTPNSR.js +20 -0
- package/dist/chunk-PPHXX4W7.js +175 -0
- package/dist/chunk-PPZESPJT.js +262 -0
- package/dist/chunk-PRDHI2L5.js +296 -0
- package/dist/chunk-PSTKWHHN.js +32 -0
- package/dist/chunk-PV53ODOT.js +555 -0
- package/dist/chunk-PVJBTI7P.js +24 -0
- package/dist/chunk-PXKERBKX.js +100 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q3DHEZA5.js +85 -0
- package/dist/chunk-Q5RZDVQK.js +204 -0
- package/dist/chunk-Q64IR2Z2.js +4933 -0
- package/dist/chunk-Q6Z5UGPL.js +398 -0
- package/dist/chunk-QAMAV4DT.js +627 -0
- package/dist/chunk-QBWF4444.js +112 -0
- package/dist/chunk-QDKYT4CI.js +22805 -0
- package/dist/chunk-QFLBRWCJ.js +126 -0
- package/dist/chunk-QILAQACE.js +107 -0
- package/dist/chunk-QJ6OEH4K.js +3683 -0
- package/dist/chunk-QK3NLYTV.js +320 -0
- package/dist/chunk-QOFLUS24.js +535 -0
- package/dist/chunk-QOIASNEJ.js +98 -0
- package/dist/chunk-QP4UTSOV.js +3587 -0
- package/dist/chunk-QP5ST7RI.js +904 -0
- package/dist/chunk-QS5AGRZN.js +167 -0
- package/dist/chunk-QTAOU53N.js +107 -0
- package/dist/chunk-QTCLJHLZ.js +351 -0
- package/dist/chunk-QTOLTG4L.js +119 -0
- package/dist/chunk-QWDV4OSL.js +142 -0
- package/dist/chunk-QWMQ74KD.js +508 -0
- package/dist/chunk-QX6LOMMH.js +158 -0
- package/dist/chunk-QXBKIHY7.js +238 -0
- package/dist/chunk-QXBKTJT6.js +270 -0
- package/dist/chunk-QYQOVOLR.js +196 -0
- package/dist/chunk-R2CKS6WT.js +73 -0
- package/dist/chunk-R2TIYG7U.js +68 -0
- package/dist/chunk-R2W3LESH.js +465 -0
- package/dist/chunk-R5EXHMUN.js +100 -0
- package/dist/chunk-RBBTBCHX.js +289 -0
- package/dist/chunk-RBKLOUXA.js +121 -0
- package/dist/chunk-RCP2Y6DQ.js +86 -0
- package/dist/chunk-RCYEJSD5.js +260 -0
- package/dist/chunk-RFDVSI4N.js +134 -0
- package/dist/chunk-RGJU63FV.js +1717 -0
- package/dist/chunk-RI4EX2QE.js +18 -0
- package/dist/chunk-RIMARPCF.js +152 -0
- package/dist/chunk-RJ6RSNT6.js +42 -0
- package/dist/chunk-RNI2GUIF.js +131 -0
- package/dist/chunk-RPHUWJ3K.js +264 -0
- package/dist/chunk-RPIMAMV7.js +361 -0
- package/dist/chunk-RQFX6YRN.js +49 -0
- package/dist/chunk-RRC5BL7B.js +285 -0
- package/dist/chunk-RSYTSBOO.js +79 -0
- package/dist/chunk-RWQQIKIC.js +35 -0
- package/dist/chunk-RZUBOZH2.js +373 -0
- package/dist/chunk-S2E632FQ.js +380 -0
- package/dist/chunk-S2VQCZO4.js +22 -0
- package/dist/chunk-S2XIMQJQ.js +681 -0
- package/dist/chunk-S47ISY27.js +183 -0
- package/dist/chunk-S6I4KISU.js +142 -0
- package/dist/chunk-S6TAFQI5.js +2403 -0
- package/dist/chunk-SBBNT5ZJ.js +27 -0
- package/dist/chunk-SDJV2DTQ.js +491 -0
- package/dist/chunk-SDPIFNLD.js +33 -0
- package/dist/chunk-SF6YK3JI.js +835 -0
- package/dist/chunk-SK3ZFONO.js +976 -0
- package/dist/chunk-SKTFRFQ7.js +1775 -0
- package/dist/chunk-SNGM452K.js +2002 -0
- package/dist/chunk-STW7OFOA.js +581 -0
- package/dist/chunk-SU6UFUQY.js +1351 -0
- package/dist/chunk-SVJM3LJA.js +27 -0
- package/dist/chunk-SVUNZOPG.js +129 -0
- package/dist/chunk-T5XLUYV3.js +34 -0
- package/dist/chunk-T7E5JS6N.js +57 -0
- package/dist/chunk-TA3N5MMO.js +450 -0
- package/dist/chunk-TAJPCXSB.js +13 -0
- package/dist/chunk-TDEMDRN2.js +1639 -0
- package/dist/chunk-TDSEX5CF.js +24 -0
- package/dist/chunk-TGCVL74Y.js +28 -0
- package/dist/chunk-TGXXYDL4.js +1012 -0
- package/dist/chunk-TIKTYUJO.js +296 -0
- package/dist/chunk-TIZAJD4M.js +6029 -0
- package/dist/chunk-TLSEZZCG.js +5549 -0
- package/dist/chunk-TMBDRHAX.js +19 -0
- package/dist/chunk-TRURQUBP.js +1217 -0
- package/dist/chunk-TSQ256N5.js +152 -0
- package/dist/chunk-TUQMMFEQ.js +2244 -0
- package/dist/chunk-U424MJLC.js +81 -0
- package/dist/chunk-U4DRS6ZW.js +11 -0
- package/dist/chunk-U5KS22IN.js +204 -0
- package/dist/chunk-UAPHFJPN.js +673 -0
- package/dist/chunk-UB7LQITD.js +1040 -0
- package/dist/chunk-UBVRHGAZ.js +237 -0
- package/dist/chunk-UDNNAQN5.js +98 -0
- package/dist/chunk-UJP3XM54.js +342 -0
- package/dist/chunk-UJYDH2MW.js +305 -0
- package/dist/chunk-UKMPASIC.js +399 -0
- package/dist/chunk-UKUL3EHK.js +763 -0
- package/dist/chunk-UPHU64NB.js +1327 -0
- package/dist/chunk-URMSZXAD.js +43 -0
- package/dist/chunk-US3RT6CY.js +525 -0
- package/dist/chunk-UTDEOPKV.js +30 -0
- package/dist/chunk-UUUDKV7N.js +13 -0
- package/dist/chunk-UVKWU3SM.js +39 -0
- package/dist/chunk-V2TVPO3X.js +521 -0
- package/dist/chunk-V3KLVK7B.js +398 -0
- package/dist/chunk-V3YFFHDR.js +17 -0
- package/dist/chunk-VAG6GUBR.js +1162 -0
- package/dist/chunk-VBENAHDA.js +1814 -0
- package/dist/chunk-VBNCKCCI.js +16 -0
- package/dist/chunk-VCGSWN5M.js +1300 -0
- package/dist/chunk-VE2O2I7M.js +115 -0
- package/dist/chunk-VIFHYTI5.js +31 -0
- package/dist/chunk-VJER3FUS.js +5080 -0
- package/dist/chunk-VLMDM56Q.js +268 -0
- package/dist/chunk-VMFZEVVX.js +99 -0
- package/dist/chunk-VNDPQVUZ.js +1853 -0
- package/dist/chunk-VSPJRLGA.js +4783 -0
- package/dist/chunk-VSWC5ZXI.js +103 -0
- package/dist/chunk-VYI4DSZP.js +2017 -0
- package/dist/chunk-W4BAIQ6L.js +1212 -0
- package/dist/chunk-W7VQAL2K.js +2203 -0
- package/dist/chunk-WEI3ZDZH.js +33 -0
- package/dist/chunk-WEIUS5NT.js +26 -0
- package/dist/chunk-WEW7UFWE.js +546 -0
- package/dist/chunk-WGQAXLNA.js +384 -0
- package/dist/chunk-WJAL4JL3.js +206 -0
- package/dist/chunk-WKFOJ5ZG.js +141 -0
- package/dist/chunk-WMWQEFLN.js +2177 -0
- package/dist/chunk-WNECKKO2.js +154 -0
- package/dist/chunk-WPKLYOAM.js +195 -0
- package/dist/chunk-WSH37YQN.js +243 -0
- package/dist/chunk-WV3KQDPE.js +84 -0
- package/dist/chunk-WZFC4V4U.js +389 -0
- package/dist/chunk-XCOIBNET.js +13 -0
- package/dist/chunk-XDM65LBX.js +53 -0
- package/dist/chunk-XGU6PBO5.js +645 -0
- package/dist/chunk-XHOI4V62.js +43 -0
- package/dist/chunk-XIBVFMLQ.js +6858 -0
- package/dist/chunk-XJ2E7YEJ.js +117 -0
- package/dist/chunk-XON4ZCYX.js +69 -0
- package/dist/chunk-XQPEWJPI.js +127 -0
- package/dist/chunk-XSJTIFUY.js +31 -0
- package/dist/chunk-XT7OVWM3.js +64 -0
- package/dist/chunk-XTXYIISX.js +1104 -0
- package/dist/chunk-XV7NZL4B.js +57 -0
- package/dist/chunk-XWIZ6B5S.js +109 -0
- package/dist/chunk-XWLIKOBX.js +240 -0
- package/dist/chunk-XZOXEIN4.js +156 -0
- package/dist/chunk-XZRS7I7W.js +3673 -0
- package/dist/chunk-Y4ZDDHB6.js +44 -0
- package/dist/chunk-Y733RB7T.js +194 -0
- package/dist/chunk-YBHUB4XR.js +14931 -0
- package/dist/chunk-YKC75XK4.js +147 -0
- package/dist/chunk-YKMXFVK7.js +27 -0
- package/dist/chunk-YO5BAJ55.js +225 -0
- package/dist/chunk-YPDCLXNZ.js +568 -0
- package/dist/chunk-YSQPGVFK.js +116 -0
- package/dist/chunk-YTVVTIWJ.js +716 -0
- package/dist/chunk-YVCMDVBB.js +698 -0
- package/dist/chunk-YYTOA6DP.js +114 -0
- package/dist/chunk-Z5WGNULV.js +88 -0
- package/dist/chunk-Z6NGVVXG.js +66 -0
- package/dist/chunk-Z7CDQNH3.js +20 -0
- package/dist/chunk-ZH4G3FU5.js +187 -0
- package/dist/chunk-ZJB6SQPA.js +257 -0
- package/dist/chunk-ZL6E4HMY.js +42 -0
- package/dist/chunk-ZMLOC3DW.js +62 -0
- package/dist/chunk-ZORW64QH.js +101 -0
- package/dist/chunk-ZVVD2XGV.js +1369 -0
- package/dist/cli-utils-ER33ODC5.js +31 -0
- package/dist/cli-validation-6CHPTANV.js +146 -0
- package/dist/commit-utils-3STAY7C7.js +49 -0
- package/dist/compatibility-notice-IHNXR2JR.js +21 -0
- package/dist/compete-config-resolver-V3GDNJMM.js +125 -0
- package/dist/compete-results-XUM3PJHS.js +318 -0
- package/dist/competition-outcome-tracker-NWSMVUPG.js +62 -0
- package/dist/config-4HHE4OIK.js +238 -0
- package/dist/config-D6YAZASH.js +10 -0
- package/dist/config-MIPRDYYV.js +46 -0
- package/dist/config-audit-GA5DCUAI.js +371 -0
- package/dist/config-explain-4LMO556H.js +71 -0
- package/dist/config-loader-RGP634PC.js +51 -0
- package/dist/config-manager-J2UTO635.js +114 -0
- package/dist/config-merger-TVUJBZTS.js +14 -0
- package/dist/config-validator-H4ZVIE56.js +122 -0
- package/dist/cost-HSIV5U2Y.js +85 -0
- package/dist/data-migration-state-OAUL2WPQ.js +35 -0
- package/dist/db-migrator-NUOW5NKR.js +34 -0
- package/dist/default-paths-UILB62BN.js +49 -0
- package/dist/discovery-V6GYUJUC.js +53 -0
- package/dist/doctor-BBSMCDWE.js +271 -0
- package/dist/doctor-context-blocks-LPY75VZA.js +231 -0
- package/dist/doctor-instructions-import-JXHDDOE5.js +70 -0
- package/dist/doctor-purpose-6YJOXPBE.js +30 -0
- package/dist/domain-analyzer-S35ABUNA.js +48 -0
- package/dist/ensure-initialized-Q6PEU55O.js +78 -0
- package/dist/entry.js +922 -0
- package/dist/error-parser-core-MZFMFANN.js +70 -0
- package/dist/evaluation-context-GQBYDAEV.js +63 -0
- package/dist/evidence-plan-resolver-5FUAP3SA.js +111 -0
- package/dist/execution-data-writer-YCG5UH37.js +72 -0
- package/dist/execution-policy-applier-PWZM5LNA.js +63 -0
- package/dist/execution-preflight-DASJWWQF.js +223 -0
- package/dist/execution-roles-KNSNNB2D.js +112 -0
- package/dist/exit-code-error-QIY5DOKF.js +18 -0
- package/dist/external-temp-cleanup-CAW7LQ6R.js +360 -0
- package/dist/factory-YCYCB7AE.js +209 -0
- package/dist/file-utils-ZQYNBQBW.js +45 -0
- package/dist/flywheel-allowlist-revert-H22HA6JE.js +21 -0
- package/dist/flywheel-autoloop-executor-PDOASJEZ.js +316 -0
- package/dist/flywheel-autoloop-unverified-TOKYFCDC.js +36 -0
- package/dist/flywheel-competition-executor-YJUMMEJO.js +320 -0
- package/dist/flywheel-executor-JWU43KAY.js +437 -0
- package/dist/flywheel-git-isolation-FQ2TRWMB.js +56 -0
- package/dist/flywheel-manifest-HDPNMWWF.js +114 -0
- package/dist/flywheel-model-defaults-UHAKTZT3.js +110 -0
- package/dist/flywheel-preflight-III2C7TH.js +163 -0
- package/dist/flywheel-resume-2O45MS7Y.js +31 -0
- package/dist/flywheel-resume-execution-lease-P2BK257J.js +59 -0
- package/dist/flywheel-safety-AVAVFXIJ.js +19 -0
- package/dist/flywheel-scope-decision-ALJUUNGG.js +14 -0
- package/dist/get-telemetry-logger-MPDVO2JA.js +155 -0
- package/dist/git-message-style-KCXGMTDT.js +104 -0
- package/dist/guidance-profile-config-G3K5YQOO.js +113 -0
- package/dist/guidance-profile-renderer-TX4TNDFL.js +23 -0
- package/dist/identity-XLQJ4YJF.js +25 -0
- package/dist/index-query-6NUY4FKX.js +37 -0
- package/dist/index.js +85588 -0
- package/dist/indexer-DL54CM3K.js +44 -0
- package/dist/init-OUZ72344.js +245 -0
- package/dist/instructions-import-offer-NSYXSITC.js +363 -0
- package/dist/instructions-run-NBII7W36.js +114 -0
- package/dist/interrupted-work-E4TLKJLE.js +43 -0
- package/dist/invoke-provider-auth-NEWEIHCI.js +38 -0
- package/dist/judge-scoring.json +64 -0
- package/dist/lab-mode-YXQ2Y3NG.js +44 -0
- package/dist/lab-utils-5QAMDJR5.js +29 -0
- package/dist/linux-host-class-YDWQX65O.js +31 -0
- package/dist/llm-judge-executor-OJSNCTFJ.js +179 -0
- package/dist/local-ci-config-VZ4WWDT5.js +48 -0
- package/dist/local-executor-DCMDDRX2.js +280 -0
- package/dist/logger-5KNKS3O4.js +154 -0
- package/dist/matrix-prompt-builder-4ZBOGZSB.js +527 -0
- package/dist/matrix-result-parser-EZJM4YXP.js +14 -0
- package/dist/merge-batch-RTEEMVDB.js +313 -0
- package/dist/merge-batch-members-VIWGVRHP.js +24 -0
- package/dist/merge-queue-TIIUSOY6.js +126 -0
- package/dist/merge-queue-trust-DUNZ4J65.js +21 -0
- package/dist/metadata-UKSMWM6J.js +29 -0
- package/dist/model-catalog-readiness-R5KJBP5M.js +202 -0
- package/dist/model-invocations-db-XTUPPQ34.js +70 -0
- package/dist/model-resolver-JFZ2MHOJ.js +39 -0
- package/dist/model-resolver-SCQ57KAQ.js +217 -0
- package/dist/model-roles-D3OR4NAH.js +110 -0
- package/dist/monitor-F4SIU5G2.js +96 -0
- package/dist/nested-worktrees-2WUWQ75N.js +23 -0
- package/dist/next-S3IRLVER.js +96 -0
- package/dist/next-UKPFRXOO.js +148 -0
- package/dist/opportunistic-TL4S26VX.js +86 -0
- package/dist/optimization-config-resolver-NAEIYU6A.js +148 -0
- package/dist/package-HJR4QJT4.js +10 -0
- package/dist/parse-git-status-7PIYFPS2.js +21 -0
- package/dist/path-security-5WJSY56G.js +41 -0
- package/dist/paths-JUI7C4KU.js +45 -0
- package/dist/plan-backlog-list-fast-SSTQL5XP.js +89 -0
- package/dist/plan-backlog-show-fast-PFRAQ3JB.js +114 -0
- package/dist/plan-sprint-list-fast-HD2KBZSY.js +110 -0
- package/dist/plan-sprint-status-fast-YFYH7WQF.js +116 -0
- package/dist/plan-task-status-fast-JU5PNLUU.js +114 -0
- package/dist/plan-to-flywheel-PY5MSY2H.js +529 -0
- package/dist/preflight-PJHGTJNZ.js +12 -0
- package/dist/process-registry-B4ZUCN7T.js +28 -0
- package/dist/process-scanner-TU3QHWVB.js +23 -0
- package/dist/processes-6FHVGT72.js +28 -0
- package/dist/processes-json-fast-YWTMUWXB.js +72 -0
- package/dist/profile-HWTC56SG.js +209 -0
- package/dist/project-runtime-paths-2UQNG5Y4.js +41 -0
- package/dist/protected-branches-N36LDBFY.js +71 -0
- package/dist/provider-aware-resource-manager-YTFAHNGJ.js +40 -0
- package/dist/provider-matrix-2KO2V5OM.js +214 -0
- package/dist/provider-matrix-renderer-MEC3PER7.js +139 -0
- package/dist/provider-output-forwarding-2AHL7YMA.js +26 -0
- package/dist/provider-registry-WCZ65QHC.js +136 -0
- package/dist/provider-temp-cleanup-IUNPJCZF.js +14 -0
- package/dist/prune-engine-EOQVWEP6.js +448 -0
- package/dist/publication-turn-S42RZTQF.js +44 -0
- package/dist/quality-gate-JQMVCKOO.js +72 -0
- package/dist/quickstart-3O6FRCFB.js +105 -0
- package/dist/readiness-GW6ABRHB.js +139 -0
- package/dist/readiness-KFZCXDTT.js +215 -0
- package/dist/registry-7XG376SY.js +217 -0
- package/dist/repair-helpers-TTV5FEF4.js +117 -0
- package/dist/repo-root-YG6MQZDW.js +28 -0
- package/dist/resolution-engine-FFEK7HQ2.js +187 -0
- package/dist/resolve-provider-cli-ZEXVZKET.js +44 -0
- package/dist/restore-telemetry-db-E7BR5PO7.js +459 -0
- package/dist/routing-events-AW6SRZ6G.js +13 -0
- package/dist/routing-history-store-FMDDQL6W.js +39 -0
- package/dist/routing-planner-C6OHO3EY.js +221 -0
- package/dist/runner-KHDNQ5KU.js +1194 -0
- package/dist/runner-PIS2BW7Y.js +1801 -0
- package/dist/safety-E3JJVSFL.js +67 -0
- package/dist/safety-stanza-HE6GXP4G.js +49 -0
- package/dist/sandbox-7LBZC5PE.js +72 -0
- package/dist/sandbox-ASWCDYZL.js +80 -0
- package/dist/schema-extensions.sql +234 -0
- package/dist/security-JI22TBEB.js +17 -0
- package/dist/setup-bb-plugin-FQXLA76Y.js +531 -0
- package/dist/setup-claude-code-NF4ZSHVE.js +197 -0
- package/dist/setup-temp-cleanup-2APZ4HMA.js +29 -0
- package/dist/shared-utils-34HJZ6JX.js +40 -0
- package/dist/simple-artifact-goal-MQLDDMMX.js +29 -0
- package/dist/sprint-N7JFTEGL.js +403 -0
- package/dist/sprint-bridge-KJXIBNRJ.js +117 -0
- package/dist/sprint-completion-dependents-34SR7JVL.js +43 -0
- package/dist/sprint-execution-service-A6JPUBWY.js +376 -0
- package/dist/sprint-manager-MU2ZUWN2.js +140 -0
- package/dist/sqlite-timeout-config-U2CT4LKU.js +13 -0
- package/dist/sqlite-wrapper-FSGJ2QEW.js +19 -0
- package/dist/stale-cleanup-orchestrator-4X6324VF.js +71 -0
- package/dist/storage-access-I3V5LER7.js +31 -0
- package/dist/store-XVG64ZR2.js +25 -0
- package/dist/task-classifier-MN2DYE5R.js +558 -0
- package/dist/task-list-fast-7AT6LTTA.js +112 -0
- package/dist/task-manager-LHPBBRRE.js +136 -0
- package/dist/task-report-OLPAWBTK.js +64 -0
- package/dist/task-type-detector-6RG3YNZF.js +27 -0
- package/dist/task-type-resolver-EQHOUIHT.js +64 -0
- package/dist/telemetry-5VYSK772.js +212 -0
- package/dist/telemetry-health-tracker-4GFW2EMP.js +12 -0
- package/dist/telemetry-path-resolver-H3NFHHQY.js +34 -0
- package/dist/telemetry-reliability-URJDTNPZ.js +76 -0
- package/dist/transports-NREZQTJK.js +106 -0
- package/dist/unified-cost-tracker-5MTBJ2CL.js +59 -0
- package/dist/unified-state-cleanup-GGAXLHXF.js +292 -0
- package/dist/universal-cost-calculator-JTFEJVWK.js +70 -0
- package/dist/user-agents-QLIGPLP2.js +30 -0
- package/dist/variant-execution-I4ZZUAWY.js +319 -0
- package/dist/variant-opt-parser-JHCXKY2K.js +251 -0
- package/dist/variant-request-input-PL37N3AN.js +11 -0
- package/dist/variant-state-cleanup-KHMXRBZN.js +287 -0
- package/dist/variant-worker-COB5YTJX.js +4517 -0
- package/dist/variant-worker.js +26 -0
- package/dist/variants-XGDSXYG3.js +267 -0
- package/dist/verification-command-YSPU4VBH.js +66 -0
- package/dist/verify-command-timeout-DQUYH33Z.js +11 -0
- package/dist/verify-commands-LOHMOFKC.js +65 -0
- package/dist/warning-JSKSFKBT.js +13 -0
- package/dist/work-edge-K65NHVWY.js +50 -0
- package/dist/work-item-writer-KY5ZWZWW.js +30 -0
- package/dist/worker-JNXXEIT6.js +1504 -0
- package/dist/worker.js +21 -0
- package/dist/workspace-tree-identity-JO5PSKTW.js +52 -0
- package/dist/worktree-environment-WTVABAQW.js +64 -0
- package/dist/worktree-metrics-VZRQI26T.js +66 -0
- package/dist/writeback-TQFQBJNU.js +50 -0
- package/docs/CLI_REFERENCE.md +8287 -0
- package/docs/PROVIDER_SETUP.md +1737 -0
- package/docs/README.md +96 -0
- package/docs/TROUBLESHOOTING.md +2209 -0
- package/docs/getting-started/QUICK_START.md +260 -0
- package/docs/getting-started/README.md +198 -0
- package/docs/getting-started/SETUP.md +25 -0
- package/docs/reference/AGENT_CONTEXT.md +84 -0
- package/docs/reference/DELIVERY_ONBOARDING.md +198 -0
- package/docs/reference/PROVIDER_RELEASE_TIERS.md +34 -0
- package/docs/reference/SECURITY.md +291 -0
- package/docs/reference/TELEMETRY_PRIVACY.md +259 -0
- package/integrations/bb-plugin-build/README.md +391 -0
- package/integrations/bb-plugin-build/app.css +431 -0
- package/integrations/bb-plugin-build/app.tsx +6210 -0
- package/integrations/bb-plugin-build/package.json +45 -0
- package/integrations/bb-plugin-build/server.ts +488 -0
- package/integrations/bb-plugin-build/src/adapter.ts +3672 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-model.ts +150 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-schema.ts +458 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-service.ts +467 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-view.ts +1025 -0
- package/integrations/bb-plugin-build/src/command-evidence-schema.ts +13 -0
- package/integrations/bb-plugin-build/src/competition-defaults-schema.ts +122 -0
- package/integrations/bb-plugin-build/src/competition-defaults-service.ts +105 -0
- package/integrations/bb-plugin-build/src/competition-defaults-view.ts +138 -0
- package/integrations/bb-plugin-build/src/contract.ts +718 -0
- package/integrations/bb-plugin-build/src/execution-activity-view.ts +98 -0
- package/integrations/bb-plugin-build/src/finalize-model.ts +205 -0
- package/integrations/bb-plugin-build/src/finalize-schema.ts +150 -0
- package/integrations/bb-plugin-build/src/finalize-service.ts +330 -0
- package/integrations/bb-plugin-build/src/finalize-view.ts +159 -0
- package/integrations/bb-plugin-build/src/judge-operations-readback.ts +303 -0
- package/integrations/bb-plugin-build/src/judge-operations-schema.ts +33 -0
- package/integrations/bb-plugin-build/src/judge-operations-view.ts +459 -0
- package/integrations/bb-plugin-build/src/model.ts +1318 -0
- package/integrations/bb-plugin-build/src/monitor-service.ts +233 -0
- package/integrations/bb-plugin-build/src/panel-read-ux.ts +97 -0
- package/integrations/bb-plugin-build/src/patch-preview-schema.ts +166 -0
- package/integrations/bb-plugin-build/src/patch-preview-service.ts +480 -0
- package/integrations/bb-plugin-build/src/patch-preview-view.ts +109 -0
- package/integrations/bb-plugin-build/src/planning-readback.ts +228 -0
- package/integrations/bb-plugin-build/src/planning-service.ts +647 -0
- package/integrations/bb-plugin-build/src/planning-sync-evidence.ts +37 -0
- package/integrations/bb-plugin-build/src/planning-workspace-readback.ts +33 -0
- package/integrations/bb-plugin-build/src/planning-workspace-schema.ts +131 -0
- package/integrations/bb-plugin-build/src/planning-workspace.ts +333 -0
- package/integrations/bb-plugin-build/src/poll-handoff.ts +136 -0
- package/integrations/bb-plugin-build/src/project-target-server.ts +21 -0
- package/integrations/bb-plugin-build/src/project-target.ts +45 -0
- package/integrations/bb-plugin-build/src/reference-index.ts +218 -0
- package/integrations/bb-plugin-build/src/repository-identity.ts +17 -0
- package/integrations/bb-plugin-build/src/repository-schema.ts +30 -0
- package/integrations/bb-plugin-build/src/result-explorer-view.ts +597 -0
- package/integrations/bb-plugin-build/src/result-readback-model.ts +163 -0
- package/integrations/bb-plugin-build/src/result-readback-schema.ts +319 -0
- package/integrations/bb-plugin-build/src/result-readback-service.ts +333 -0
- package/integrations/bb-plugin-build/src/run-page-view.ts +332 -0
- package/integrations/bb-plugin-build/src/setup-config-schema.ts +293 -0
- package/integrations/bb-plugin-build/src/setup-config-service.ts +620 -0
- package/integrations/bb-plugin-build/src/setup-config-view.ts +388 -0
- package/integrations/bb-plugin-build/src/sprint-close-model.ts +150 -0
- package/integrations/bb-plugin-build/src/sprint-close-schema.ts +128 -0
- package/integrations/bb-plugin-build/src/sprint-close-service.ts +293 -0
- package/integrations/bb-plugin-build/src/sprint-close-view.ts +116 -0
- package/integrations/bb-plugin-build/src/sprint-composition-model.ts +709 -0
- package/integrations/bb-plugin-build/src/sprint-composition-readback.ts +231 -0
- package/integrations/bb-plugin-build/src/sprint-composition-schema.ts +597 -0
- package/integrations/bb-plugin-build/src/sprint-composition-service.ts +773 -0
- package/integrations/bb-plugin-build/src/sprint-composition-view.ts +1221 -0
- package/integrations/bb-plugin-build/src/sprint-execution-schema.ts +188 -0
- package/integrations/bb-plugin-build/src/sprint-execution-service.ts +289 -0
- package/integrations/bb-plugin-build/src/sprint-execution-view.ts +54 -0
- package/integrations/bb-plugin-build/src/task-authoring-model.ts +563 -0
- package/integrations/bb-plugin-build/src/task-authoring-schema.ts +629 -0
- package/integrations/bb-plugin-build/src/task-authoring-service.ts +842 -0
- package/integrations/bb-plugin-build/src/task-authoring-view.ts +1227 -0
- package/integrations/bb-plugin-build/src/task-catalog-schema.ts +46 -0
- package/integrations/bb-plugin-build/src/task-catalog-service.ts +44 -0
- package/integrations/bb-plugin-build/src/task-detail-readback.ts +56 -0
- package/integrations/bb-plugin-build/src/task-detail-schema.ts +171 -0
- package/integrations/bb-plugin-build/src/task-navigation.ts +446 -0
- package/integrations/bb-plugin-build/src/workbench-route.ts +74 -0
- package/package.json +306 -4
- package/schemas/README.md +80 -0
- package/schemas/build.config.schema.json +2110 -0
- package/schemas/config-v1.schema.json +136 -0
- package/schemas/execution-config.schema.json +47 -0
- package/schemas/judge-scoring.schema.json +257 -0
- package/schemas/provider-config.schema.json +179 -0
- package/schemas/telemetry-config.schema.json +79 -0
- package/schemas/user-preferences.schema.json +141 -0
- package/scripts/assert-node-runtime.mjs +146 -0
- package/scripts/preflight-native.mjs +49 -0
- package/scripts/setup-git-hooks.mjs +39 -0
- package/scripts/sync.sh +2871 -0
- package/scripts/write-node-launcher.sh +138 -0
- package/src/resources/gemini/slash-packs/default/plan.toml +15 -0
- package/src/resources/gemini/slash-packs/default/summary.toml +16 -0
- package/src/resources/gemini/slash-packs/default/tests.toml +16 -0
- package/templates/.build-project/README.md +41 -0
- package/templates/.build-project/agents/README.md +50 -0
- package/templates/.build-project/agents/architect.md +10 -0
- package/templates/.build-project/agents/code-reviewer.md +10 -0
- package/templates/.build-project/agents/debugger.md +10 -0
- package/templates/.build-project/agents/documenter.md +10 -0
- package/templates/.build-project/agents/test-automator.md +10 -0
- package/templates/.build-project/config/build.config.jsonc +64 -0
- package/templates/.build-project/config/project-context.template.json +6 -0
- package/templates/.build-project/config/task-type-aliases.presets/kanban.yaml +14 -0
- package/templates/.build-project/config/task-type-aliases.presets/scrum.yaml +17 -0
- package/templates/.build-project/config/task-type-aliases.presets/xp.yaml +12 -0
- package/templates/.build-project/config/task-type-aliases.yaml +29 -0
- package/templates/.build-project/gitignore.template +70 -0
- package/templates/.build-project/task-types/analysis.yaml +38 -0
- package/templates/.build-project/task-types/architecture.yaml +35 -0
- package/templates/.build-project/task-types/doc.yaml +34 -0
- package/templates/.build-project/task-types/feature.yaml +22 -0
- package/templates/.build-project/task-types/general.yaml +4 -0
- package/templates/.build-project/task-types/security.yaml +38 -0
- package/templates/AGENTS.template.md +85 -0
- package/templates/CLAUDE.template.md +1 -0
- package/templates/GEMINI.template.md +1 -0
- package/templates/builtin-workflows/code-review.yaml +73 -0
- package/templates/builtin-workflows/skills/code-review/SKILL.md +61 -0
- package/templates/builtin-workflows/skills/execution-contract-analysis/SKILL.md +24 -0
- package/templates/builtin-workflows/skills/execution-contract-code/SKILL.md +19 -0
- package/templates/builtin-workflows/skills/execution-contract-structured/SKILL.md +17 -0
- package/templates/ci/incident-intake.mjs +787 -0
- package/templates/ci/incidents.yml.template +103 -0
- package/templates/ci/node.yml.template +17 -0
- package/templates/ci/python.yml.template +17 -0
- package/templates/guard-kit/GUARD_SETUP.md.template +258 -0
- package/templates/guard-kit/build-guard.yml.template +189 -0
- package/templates/guard-kit/check.mjs.template +932 -0
- package/templates/guard-kit/config.json.template +7 -0
- package/templates/guard-kit/policy.json.template +1 -0
- package/templates/profiles/README.md +57 -0
- package/templates/profiles/frontier-claude.json +23 -0
- package/templates/profiles/frontier-codex.json +23 -0
|
@@ -0,0 +1,3892 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
3
|
+
boundLocalCiValidationProfileReason,
|
|
4
|
+
normalizeAuthorityPath,
|
|
5
|
+
normalizeRepoRelativePath,
|
|
6
|
+
parseGitRawDiffChangeSet,
|
|
7
|
+
resolveLocalCiConfig,
|
|
8
|
+
resolveLocalCiFallbackCommands,
|
|
9
|
+
resolveLocalCiValidationProfileEnv,
|
|
10
|
+
resolveProjectVerificationAuthority,
|
|
11
|
+
selectLocalCiValidationProfile
|
|
12
|
+
} from "./chunk-PEBWDYIW.js";
|
|
13
|
+
import {
|
|
14
|
+
registerDetachedWorkerProcess,
|
|
15
|
+
startDetachedWorkerRegistryHeartbeat
|
|
16
|
+
} from "./chunk-P7RVCHQV.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
19
|
+
DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS,
|
|
20
|
+
DEFAULT_LOCAL_CI_STATUS_CONTEXT,
|
|
21
|
+
LocalCiStateStore,
|
|
22
|
+
MAX_LOCAL_CI_RETAINED_LOG_BYTES,
|
|
23
|
+
NODE_PACKAGE_MANAGER_MANIFESTS,
|
|
24
|
+
boundLocalCiCommandOutput,
|
|
25
|
+
computeEntrypointFingerprint,
|
|
26
|
+
createFailurePublicationLease,
|
|
27
|
+
createOutstandingFailureStatusPublication,
|
|
28
|
+
createOutstandingStatusPublication,
|
|
29
|
+
createQueuedRun,
|
|
30
|
+
hasLiveFailurePublicationLease,
|
|
31
|
+
hasOutstandingRemoteStatusPublication,
|
|
32
|
+
isPublicationPending,
|
|
33
|
+
launcherManifests,
|
|
34
|
+
matchesFailurePublicationLease,
|
|
35
|
+
outstandingStatusPublicationMatchesRecord,
|
|
36
|
+
parseLocalCiRepositoryIdentity,
|
|
37
|
+
parseScopedPin,
|
|
38
|
+
pinnedManifestFiles,
|
|
39
|
+
referencedTableScript,
|
|
40
|
+
renderBoundedLocalCiLogEntry,
|
|
41
|
+
repoStateKey,
|
|
42
|
+
scopedPinKey,
|
|
43
|
+
scriptTableUnchanged
|
|
44
|
+
} from "./chunk-VBENAHDA.js";
|
|
45
|
+
import {
|
|
46
|
+
RepositorySchema,
|
|
47
|
+
resolveGitHubRepositoryFromRemote,
|
|
48
|
+
resolveOptionalGitHubRepositoryFromRemote
|
|
49
|
+
} from "./chunk-BDPYFAB4.js";
|
|
50
|
+
import {
|
|
51
|
+
acquireFencedFileLock
|
|
52
|
+
} from "./chunk-QYQOVOLR.js";
|
|
53
|
+
import {
|
|
54
|
+
runVerificationArgv,
|
|
55
|
+
runVerificationCommand
|
|
56
|
+
} from "./chunk-S2E632FQ.js";
|
|
57
|
+
import {
|
|
58
|
+
outstandingStatusPublicationMacIsValid,
|
|
59
|
+
pruneEmptyPublicationJournalDirs
|
|
60
|
+
} from "./chunk-4U6XK2DG.js";
|
|
61
|
+
import {
|
|
62
|
+
describePipDependencyGroupGap,
|
|
63
|
+
isPipDependencyGroupInstall,
|
|
64
|
+
parsePipVersionOutput,
|
|
65
|
+
parseProjectDependencySetupCommand,
|
|
66
|
+
readFailingTestNames,
|
|
67
|
+
readVenvPipVersion,
|
|
68
|
+
selectPythonEnv
|
|
69
|
+
} from "./chunk-AHU3FKPA.js";
|
|
70
|
+
import {
|
|
71
|
+
detectEmptyRun,
|
|
72
|
+
splitTestFailureId
|
|
73
|
+
} from "./chunk-TUQMMFEQ.js";
|
|
74
|
+
import {
|
|
75
|
+
describeVerificationCommand,
|
|
76
|
+
detectExitZeroVerifyFailure,
|
|
77
|
+
isPrettierInvocation
|
|
78
|
+
} from "./chunk-EPAMVGI3.js";
|
|
79
|
+
import {
|
|
80
|
+
execGit
|
|
81
|
+
} from "./chunk-MHLEZQQI.js";
|
|
82
|
+
import {
|
|
83
|
+
createLocalCiDetachedChildEnv
|
|
84
|
+
} from "./chunk-SVJM3LJA.js";
|
|
85
|
+
import {
|
|
86
|
+
getLocalCiStateRoot,
|
|
87
|
+
localCiSchedulingEnvelopePath
|
|
88
|
+
} from "./chunk-BV3XQQLU.js";
|
|
89
|
+
import {
|
|
90
|
+
processOwnershipState,
|
|
91
|
+
readProcessIdentity
|
|
92
|
+
} from "./chunk-MT2WAIR3.js";
|
|
93
|
+
import {
|
|
94
|
+
selectVerificationToolchainEnv
|
|
95
|
+
} from "./chunk-XSJTIFUY.js";
|
|
96
|
+
import {
|
|
97
|
+
BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA,
|
|
98
|
+
BUILD_LOCAL_CI_OVERRIDE_HALT,
|
|
99
|
+
readEnv
|
|
100
|
+
} from "./chunk-7VRJHTGZ.js";
|
|
101
|
+
|
|
102
|
+
// src/core/ci/github-local-ci.ts
|
|
103
|
+
import { setTimeout as delay } from "timers/promises";
|
|
104
|
+
import { execa } from "execa";
|
|
105
|
+
import { z } from "zod";
|
|
106
|
+
var PullRequestSchema = z.object({
|
|
107
|
+
number: z.number().int().positive(),
|
|
108
|
+
headRefOid: z.string().regex(/^[a-f0-9]{40}$/i),
|
|
109
|
+
isDraft: z.boolean(),
|
|
110
|
+
baseRefName: z.string().min(1),
|
|
111
|
+
baseRefOid: z.string().regex(/^[a-f0-9]{40}$/i)
|
|
112
|
+
});
|
|
113
|
+
var CommitStatusSchema = z.object({
|
|
114
|
+
state: z.enum(["error", "failure", "pending", "success"]),
|
|
115
|
+
context: z.string(),
|
|
116
|
+
description: z.string().nullish(),
|
|
117
|
+
target_url: z.string().nullish()
|
|
118
|
+
});
|
|
119
|
+
async function runGh(args, options = {}) {
|
|
120
|
+
const result = await execa("gh", args, {
|
|
121
|
+
cwd: options.cwd,
|
|
122
|
+
input: options.input,
|
|
123
|
+
reject: false,
|
|
124
|
+
timeout: 6e4,
|
|
125
|
+
stdio: "pipe"
|
|
126
|
+
});
|
|
127
|
+
if (result.exitCode !== 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`GitHub CLI failed: ${(result.stderr || result.stdout || args.join(" ")).split("\n")[0]}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return result.stdout;
|
|
133
|
+
}
|
|
134
|
+
async function publishGitHubLocalCiStatus(input, options = {}) {
|
|
135
|
+
const repository = RepositorySchema.parse(input.repository);
|
|
136
|
+
const payload = JSON.stringify({
|
|
137
|
+
state: input.state,
|
|
138
|
+
context: input.context,
|
|
139
|
+
description: input.description.slice(0, 140)
|
|
140
|
+
});
|
|
141
|
+
const attempts = options.attempts ?? 12;
|
|
142
|
+
let lastError;
|
|
143
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
144
|
+
try {
|
|
145
|
+
await runGh(
|
|
146
|
+
["api", "--method", "POST", `repos/${repository}/statuses/${input.sha}`, "--input", "-"],
|
|
147
|
+
{ input: payload }
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
lastError = error;
|
|
152
|
+
if (attempt < attempts) await delay(options.retryDelayMs ?? 5e3);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
156
|
+
}
|
|
157
|
+
var DEFAULT_PR_DISCOVERY_ATTEMPTS = 6;
|
|
158
|
+
var DEFAULT_PR_DISCOVERY_RETRY_DELAY_MS = 2e3;
|
|
159
|
+
async function readGitHubPullRequestState(input) {
|
|
160
|
+
try {
|
|
161
|
+
const repository = RepositorySchema.parse(input.repository);
|
|
162
|
+
const output = await runGh([
|
|
163
|
+
"pr",
|
|
164
|
+
"view",
|
|
165
|
+
String(input.number),
|
|
166
|
+
"--repo",
|
|
167
|
+
repository,
|
|
168
|
+
"--json",
|
|
169
|
+
"state"
|
|
170
|
+
]);
|
|
171
|
+
return z.object({ state: z.enum(["OPEN", "CLOSED", "MERGED"]) }).parse(JSON.parse(output)).state;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function findGitHubPullRequest(input) {
|
|
177
|
+
const repository = RepositorySchema.parse(input.repository);
|
|
178
|
+
const attempts = input.attempts ?? DEFAULT_PR_DISCOVERY_ATTEMPTS;
|
|
179
|
+
const retryDelayMs = input.retryDelayMs ?? DEFAULT_PR_DISCOVERY_RETRY_DELAY_MS;
|
|
180
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
181
|
+
const output = input.number ? await runGh([
|
|
182
|
+
"pr",
|
|
183
|
+
"view",
|
|
184
|
+
String(input.number),
|
|
185
|
+
"--repo",
|
|
186
|
+
repository,
|
|
187
|
+
"--json",
|
|
188
|
+
"number,headRefOid,isDraft,baseRefName,baseRefOid"
|
|
189
|
+
]) : await runGh([
|
|
190
|
+
"pr",
|
|
191
|
+
"list",
|
|
192
|
+
"--repo",
|
|
193
|
+
repository,
|
|
194
|
+
"--head",
|
|
195
|
+
input.branch,
|
|
196
|
+
...input.base ? ["--base", input.base] : [],
|
|
197
|
+
"--state",
|
|
198
|
+
"open",
|
|
199
|
+
"--limit",
|
|
200
|
+
"1",
|
|
201
|
+
"--json",
|
|
202
|
+
"number,headRefOid,isDraft,baseRefName,baseRefOid"
|
|
203
|
+
]);
|
|
204
|
+
const pullRequest = input.number ? PullRequestSchema.parse(JSON.parse(output)) : z.array(PullRequestSchema).parse(JSON.parse(output))[0];
|
|
205
|
+
if (pullRequest) {
|
|
206
|
+
return {
|
|
207
|
+
number: pullRequest.number,
|
|
208
|
+
headSha: pullRequest.headRefOid,
|
|
209
|
+
draft: pullRequest.isDraft,
|
|
210
|
+
baseRefName: pullRequest.baseRefName,
|
|
211
|
+
baseSha: pullRequest.baseRefOid
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (attempt < attempts) await delay(retryDelayMs);
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
var OPEN_PULL_REQUEST_LIST_LIMIT = 1e3;
|
|
219
|
+
async function listGitHubOpenPullRequests(repository) {
|
|
220
|
+
const output = await runGh([
|
|
221
|
+
"pr",
|
|
222
|
+
"list",
|
|
223
|
+
"--repo",
|
|
224
|
+
RepositorySchema.parse(repository),
|
|
225
|
+
"--state",
|
|
226
|
+
"open",
|
|
227
|
+
"--limit",
|
|
228
|
+
String(OPEN_PULL_REQUEST_LIST_LIMIT),
|
|
229
|
+
"--json",
|
|
230
|
+
"number,headRefOid,isDraft,baseRefName,baseRefOid"
|
|
231
|
+
]);
|
|
232
|
+
const pullRequests = z.array(PullRequestSchema).parse(JSON.parse(output));
|
|
233
|
+
return new Map(
|
|
234
|
+
pullRequests.map((pullRequest) => [
|
|
235
|
+
pullRequest.number,
|
|
236
|
+
{
|
|
237
|
+
number: pullRequest.number,
|
|
238
|
+
headSha: pullRequest.headRefOid,
|
|
239
|
+
draft: pullRequest.isDraft,
|
|
240
|
+
baseRefName: pullRequest.baseRefName,
|
|
241
|
+
baseSha: pullRequest.baseRefOid
|
|
242
|
+
}
|
|
243
|
+
])
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
async function resolveGitHubDefaultBranch(repository) {
|
|
247
|
+
const parsed = z.object({ defaultBranchRef: z.object({ name: z.string().min(1) }) }).parse(
|
|
248
|
+
JSON.parse(
|
|
249
|
+
await runGh([
|
|
250
|
+
"repo",
|
|
251
|
+
"view",
|
|
252
|
+
RepositorySchema.parse(repository),
|
|
253
|
+
"--json",
|
|
254
|
+
"defaultBranchRef"
|
|
255
|
+
])
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
return parsed.defaultBranchRef.name;
|
|
259
|
+
}
|
|
260
|
+
async function resolveGitHubBranchSha(input) {
|
|
261
|
+
const output = await runGh([
|
|
262
|
+
"api",
|
|
263
|
+
`repos/${RepositorySchema.parse(input.repository)}/commits/${encodeURIComponent(input.branch)}`,
|
|
264
|
+
"--jq",
|
|
265
|
+
".sha"
|
|
266
|
+
]);
|
|
267
|
+
return z.string().regex(/^[a-f0-9]{40}$/i).parse(output.trim()).toLowerCase();
|
|
268
|
+
}
|
|
269
|
+
async function readGitHubCommitStatusDetail(input) {
|
|
270
|
+
const output = await runGh([
|
|
271
|
+
"api",
|
|
272
|
+
`repos/${RepositorySchema.parse(input.repository)}/commits/${input.sha}/status`
|
|
273
|
+
]);
|
|
274
|
+
const payload = z.object({ statuses: z.array(CommitStatusSchema) }).parse(JSON.parse(output));
|
|
275
|
+
const found = payload.statuses.find((status) => status.context === input.context);
|
|
276
|
+
if (!found) return { state: "missing" };
|
|
277
|
+
return {
|
|
278
|
+
state: found.state,
|
|
279
|
+
...found.description ? { description: found.description } : {},
|
|
280
|
+
...found.target_url ? { targetUrl: found.target_url } : {}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function readGitHubCommitStatus(input) {
|
|
284
|
+
return (await readGitHubCommitStatusDetail(input)).state;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/core/ci/local-ci-runtime.ts
|
|
288
|
+
import { randomUUID } from "crypto";
|
|
289
|
+
import { existsSync } from "fs";
|
|
290
|
+
import {
|
|
291
|
+
link,
|
|
292
|
+
mkdir,
|
|
293
|
+
mkdtemp,
|
|
294
|
+
readFile,
|
|
295
|
+
realpath,
|
|
296
|
+
rename,
|
|
297
|
+
rm,
|
|
298
|
+
stat,
|
|
299
|
+
unlink,
|
|
300
|
+
writeFile
|
|
301
|
+
} from "fs/promises";
|
|
302
|
+
import os from "os";
|
|
303
|
+
import path from "path";
|
|
304
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
305
|
+
import { execa as execa2 } from "execa";
|
|
306
|
+
import { z as z2 } from "zod";
|
|
307
|
+
var SlotRecordSchema = z2.object({
|
|
308
|
+
pid: z2.number().int().positive(),
|
|
309
|
+
pidIdentity: z2.string().min(1).optional(),
|
|
310
|
+
token: z2.string().uuid(),
|
|
311
|
+
acquiredAt: z2.iso.datetime()
|
|
312
|
+
});
|
|
313
|
+
var RepositoryIdSchema = z2.string().refine((value) => parseLocalCiRepositoryIdentity(value) !== null, {
|
|
314
|
+
message: "Invalid local CI repository identity"
|
|
315
|
+
});
|
|
316
|
+
var Sha40Schema = z2.string().regex(/^[a-f0-9]{40}$/i);
|
|
317
|
+
var MaxConcurrentSchema = z2.number().int().min(1).max(8);
|
|
318
|
+
var TARGET_VALIDATION_ENV_KEYS = [
|
|
319
|
+
"PATH",
|
|
320
|
+
"HOME",
|
|
321
|
+
"USER",
|
|
322
|
+
"LOGNAME",
|
|
323
|
+
"TMPDIR",
|
|
324
|
+
"TEMP",
|
|
325
|
+
"TMP",
|
|
326
|
+
"LANG",
|
|
327
|
+
"LC_ALL",
|
|
328
|
+
"LC_CTYPE",
|
|
329
|
+
"TERM",
|
|
330
|
+
"COLORTERM",
|
|
331
|
+
"SHELL",
|
|
332
|
+
"XDG_CACHE_HOME",
|
|
333
|
+
"XDG_CONFIG_HOME",
|
|
334
|
+
"XDG_DATA_HOME",
|
|
335
|
+
"XDG_STATE_HOME"
|
|
336
|
+
];
|
|
337
|
+
var SchedulingEnvelopeSchema = z2.object({
|
|
338
|
+
schemaVersion: z2.literal(1),
|
|
339
|
+
maxConcurrent: MaxConcurrentSchema,
|
|
340
|
+
updatedFromBaseSha: Sha40Schema,
|
|
341
|
+
updatedAt: z2.iso.datetime()
|
|
342
|
+
});
|
|
343
|
+
var UNPARSABLE_SLOT_LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
344
|
+
var PRIORITY_WAIT_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
345
|
+
var DEFAULT_ENVELOPE_BASE_SHA = "0".repeat(40);
|
|
346
|
+
function createLocalCiTargetValidationEnv(source = process.env, options = {}) {
|
|
347
|
+
const env = selectVerificationToolchainEnv(source);
|
|
348
|
+
for (const key of TARGET_VALIDATION_ENV_KEYS) {
|
|
349
|
+
const value = source[key];
|
|
350
|
+
if (typeof value === "string" && value.length > 0) {
|
|
351
|
+
env[key] = value;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
env.CI = "1";
|
|
355
|
+
env.npm_config_script_shell = "/bin/sh";
|
|
356
|
+
const nodeExtraCaCerts = source.NODE_EXTRA_CA_CERTS;
|
|
357
|
+
if (typeof nodeExtraCaCerts === "string" && nodeExtraCaCerts.length > 0) {
|
|
358
|
+
env.NODE_EXTRA_CA_CERTS = nodeExtraCaCerts;
|
|
359
|
+
}
|
|
360
|
+
if (options.trustedBaseSha !== void 0) {
|
|
361
|
+
env.BUILD_RELEASE_GATE_TRUSTED_BASE = Sha40Schema.parse(options.trustedBaseSha).toLowerCase();
|
|
362
|
+
}
|
|
363
|
+
env.BUILD_LOCAL_CI_PROFILE = resolveLocalCiValidationProfileEnv(options.validationProfile);
|
|
364
|
+
if (options.gateRetry !== void 0) {
|
|
365
|
+
env.BUILD_GATE_RETRY_MANIFEST_DIGEST = options.gateRetry.manifestDigest;
|
|
366
|
+
env.BUILD_GATE_RETRY_LABELS = JSON.stringify(options.gateRetry.rerunLabels);
|
|
367
|
+
env.BUILD_GATE_REUSED_LABELS = JSON.stringify(options.gateRetry.reusedLabels);
|
|
368
|
+
}
|
|
369
|
+
return env;
|
|
370
|
+
}
|
|
371
|
+
function isNodeErrorCode(error, code) {
|
|
372
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
373
|
+
}
|
|
374
|
+
function normalizeMaxConcurrent(value) {
|
|
375
|
+
return MaxConcurrentSchema.parse(value);
|
|
376
|
+
}
|
|
377
|
+
function defaultSchedulingEnvelope(now) {
|
|
378
|
+
return {
|
|
379
|
+
schemaVersion: 1,
|
|
380
|
+
maxConcurrent: DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
381
|
+
updatedFromBaseSha: DEFAULT_ENVELOPE_BASE_SHA,
|
|
382
|
+
updatedAt: (now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async function readSchedulingEnvelope(input) {
|
|
386
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
387
|
+
const filePath = localCiSchedulingEnvelopePath(stateRoot, repoStateKey(input.repository));
|
|
388
|
+
try {
|
|
389
|
+
const raw = await readFile(filePath, "utf8");
|
|
390
|
+
return SchedulingEnvelopeSchema.parse(JSON.parse(raw));
|
|
391
|
+
} catch {
|
|
392
|
+
return defaultSchedulingEnvelope();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function writeSchedulingEnvelope(input) {
|
|
396
|
+
const repository = parseLocalCiRepositoryIdentity(RepositoryIdSchema.parse(input.repository));
|
|
397
|
+
if (!repository) {
|
|
398
|
+
throw new Error(`Invalid local CI repository identity: ${input.repository}`);
|
|
399
|
+
}
|
|
400
|
+
const envelope = SchedulingEnvelopeSchema.parse({
|
|
401
|
+
schemaVersion: 1,
|
|
402
|
+
maxConcurrent: input.maxConcurrent,
|
|
403
|
+
updatedFromBaseSha: input.updatedFromBaseSha.toLowerCase(),
|
|
404
|
+
updatedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
405
|
+
});
|
|
406
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
407
|
+
const filePath = localCiSchedulingEnvelopePath(stateRoot, repoStateKey(repository));
|
|
408
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
|
|
409
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
410
|
+
try {
|
|
411
|
+
await writeFile(temporary, `${JSON.stringify(envelope, null, 2)}
|
|
412
|
+
`, {
|
|
413
|
+
encoding: "utf8",
|
|
414
|
+
mode: 384
|
|
415
|
+
});
|
|
416
|
+
await rename(temporary, filePath);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
await unlink(temporary).catch(() => void 0);
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
return envelope;
|
|
422
|
+
}
|
|
423
|
+
async function resolveEffectiveMaxConcurrent(input) {
|
|
424
|
+
if (input.maxConcurrent !== void 0) {
|
|
425
|
+
return normalizeMaxConcurrent(input.maxConcurrent);
|
|
426
|
+
}
|
|
427
|
+
const envelope = await readSchedulingEnvelope({
|
|
428
|
+
repository: input.repository,
|
|
429
|
+
stateRoot: input.stateRoot
|
|
430
|
+
});
|
|
431
|
+
return envelope.maxConcurrent;
|
|
432
|
+
}
|
|
433
|
+
function priorityWaiterPath(input) {
|
|
434
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
435
|
+
return path.join(stateRoot, repoStateKey(input.repository), "slots", "priority.wait");
|
|
436
|
+
}
|
|
437
|
+
async function acquireLocalCiSlot(input) {
|
|
438
|
+
if (!input.priority) return pollLocalCiSlot(input);
|
|
439
|
+
const filePath = priorityWaiterPath(input);
|
|
440
|
+
const token = randomUUID();
|
|
441
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
|
|
442
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
443
|
+
await writeFile(
|
|
444
|
+
temporary,
|
|
445
|
+
`${JSON.stringify({
|
|
446
|
+
pid: process.pid,
|
|
447
|
+
pidIdentity: readProcessIdentity(process.pid),
|
|
448
|
+
token,
|
|
449
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
450
|
+
})}
|
|
451
|
+
`,
|
|
452
|
+
{ encoding: "utf8", mode: 384 }
|
|
453
|
+
);
|
|
454
|
+
await rename(temporary, filePath);
|
|
455
|
+
try {
|
|
456
|
+
return await pollLocalCiSlot(input);
|
|
457
|
+
} finally {
|
|
458
|
+
try {
|
|
459
|
+
const marker = SlotRecordSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
460
|
+
if (marker.token === token) await unlink(filePath);
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function priorityWaiterLive(filePath) {
|
|
466
|
+
let marker;
|
|
467
|
+
try {
|
|
468
|
+
marker = SlotRecordSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
if (Date.now() - Date.parse(marker.acquiredAt) > PRIORITY_WAIT_MAX_MS) return false;
|
|
473
|
+
return processOwnershipState(marker.pid, marker.pidIdentity, marker.acquiredAt) !== "reclaimable";
|
|
474
|
+
}
|
|
475
|
+
async function pollLocalCiSlot(input) {
|
|
476
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
477
|
+
const effectiveCap = await resolveEffectiveMaxConcurrent({
|
|
478
|
+
repository: input.repository,
|
|
479
|
+
stateRoot,
|
|
480
|
+
maxConcurrent: input.maxConcurrent
|
|
481
|
+
});
|
|
482
|
+
const slotsDirectory = path.join(stateRoot, repoStateKey(input.repository), "slots");
|
|
483
|
+
await mkdir(slotsDirectory, { recursive: true, mode: 448 });
|
|
484
|
+
const token = randomUUID();
|
|
485
|
+
const pidIdentity = readProcessIdentity(process.pid);
|
|
486
|
+
const deadline = Date.now() + (input.timeoutMs ?? 6 * 60 * 60 * 1e3);
|
|
487
|
+
while (Date.now() < deadline) {
|
|
488
|
+
await input.checkContinue?.();
|
|
489
|
+
if (Date.now() >= deadline) break;
|
|
490
|
+
if (!input.priority && await priorityWaiterLive(priorityWaiterPath(input))) {
|
|
491
|
+
await delay2(input.pollMs ?? 2e3);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
for (let slot = 0; slot < effectiveCap; slot += 1) {
|
|
495
|
+
const filePath = path.join(slotsDirectory, `${slot}.lock`);
|
|
496
|
+
const payload = `${JSON.stringify({
|
|
497
|
+
pid: process.pid,
|
|
498
|
+
pidIdentity,
|
|
499
|
+
token,
|
|
500
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
501
|
+
})}
|
|
502
|
+
`;
|
|
503
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
504
|
+
try {
|
|
505
|
+
try {
|
|
506
|
+
await writeFile(temporary, payload, { encoding: "utf8", mode: 384 });
|
|
507
|
+
await link(temporary, filePath);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
await unlink(temporary).catch(() => void 0);
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
await unlink(temporary).catch(() => void 0);
|
|
513
|
+
return {
|
|
514
|
+
slot,
|
|
515
|
+
maxConcurrent: effectiveCap,
|
|
516
|
+
async release() {
|
|
517
|
+
try {
|
|
518
|
+
const current = SlotRecordSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
519
|
+
if (current.token === token && current.pid === process.pid) await unlink(filePath);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (!isNodeErrorCode(error, "ENOENT")) {
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (!isNodeErrorCode(error, "EEXIST")) {
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const current = SlotRecordSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
533
|
+
const ownership = processOwnershipState(
|
|
534
|
+
current.pid,
|
|
535
|
+
current.pidIdentity,
|
|
536
|
+
current.acquiredAt
|
|
537
|
+
);
|
|
538
|
+
if (ownership === "reclaimable") {
|
|
539
|
+
await unlink(filePath);
|
|
540
|
+
}
|
|
541
|
+
} catch (readError) {
|
|
542
|
+
if (isNodeErrorCode(readError, "ENOENT")) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const metadata = await stat(filePath);
|
|
547
|
+
if (Date.now() - metadata.mtimeMs > UNPARSABLE_SLOT_LOCK_STALE_MS) {
|
|
548
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
549
|
+
if (!isNodeErrorCode(unlinkError, "ENOENT")) throw unlinkError;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
} catch (statError) {
|
|
553
|
+
if (!isNodeErrorCode(statError, "ENOENT")) throw statError;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
await delay2(input.pollMs ?? 2e3);
|
|
559
|
+
}
|
|
560
|
+
throw new Error("Timed out waiting for a local CI execution slot.");
|
|
561
|
+
}
|
|
562
|
+
var LOCAL_CI_WORKTREE_CREATION_TIMEOUT_MS = 12e4;
|
|
563
|
+
async function createLocalCiWorktree(repoRoot, sha) {
|
|
564
|
+
const parent = await mkdtemp(path.join(os.tmpdir(), "build-local-ci-"));
|
|
565
|
+
const worktreePath = path.join(parent, "worktree");
|
|
566
|
+
try {
|
|
567
|
+
const result = await execGit(["worktree", "add", "--detach", worktreePath, sha], {
|
|
568
|
+
cwd: repoRoot,
|
|
569
|
+
timeoutMs: LOCAL_CI_WORKTREE_CREATION_TIMEOUT_MS
|
|
570
|
+
});
|
|
571
|
+
if (result.exitCode !== 0) {
|
|
572
|
+
throw new Error(`Unable to create local CI worktree: ${result.stderr || result.stdout}`);
|
|
573
|
+
}
|
|
574
|
+
return worktreePath;
|
|
575
|
+
} catch (error) {
|
|
576
|
+
try {
|
|
577
|
+
await removeLocalCiWorktree(repoRoot, worktreePath);
|
|
578
|
+
} catch (cleanupError) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`${error instanceof Error ? error.message : String(error)}. Retained partial local CI worktree at ${worktreePath}: cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
|
|
581
|
+
{ cause: error }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function isRegisteredWorktree(repoRoot, worktreePath) {
|
|
588
|
+
const listed = await execGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
|
|
589
|
+
if (listed.exitCode !== 0) return false;
|
|
590
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
591
|
+
worktreePath,
|
|
592
|
+
await realpath(worktreePath).catch(() => worktreePath)
|
|
593
|
+
]);
|
|
594
|
+
return listed.stdout.split("\n").some((line) => line.startsWith("worktree ") && candidates.has(line.slice("worktree ".length)));
|
|
595
|
+
}
|
|
596
|
+
async function removeLocalCiWorktree(repoRoot, worktreePath) {
|
|
597
|
+
const resolved = path.resolve(worktreePath);
|
|
598
|
+
if (path.basename(resolved) !== "worktree" || !path.basename(path.dirname(resolved)).startsWith("build-local-ci-")) {
|
|
599
|
+
throw new Error(`Refusing to remove non-local-CI worktree path: ${resolved}`);
|
|
600
|
+
}
|
|
601
|
+
const underTemporaryRoot = resolved.startsWith(
|
|
602
|
+
`${path.resolve(os.tmpdir())}${path.sep}build-local-ci-`
|
|
603
|
+
);
|
|
604
|
+
if (!underTemporaryRoot && !await isRegisteredWorktree(repoRoot, resolved)) {
|
|
605
|
+
if (!existsSync(resolved)) return;
|
|
606
|
+
throw new Error(`Refusing to remove unregistered local CI worktree path: ${resolved}`);
|
|
607
|
+
}
|
|
608
|
+
const { removeWorktreeNestedAware } = await import("./nested-worktrees-2WUWQ75N.js");
|
|
609
|
+
const removal = await removeWorktreeNestedAware(resolved, { force: true, repoCwd: repoRoot });
|
|
610
|
+
if (removal.refused) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`Unable to unregister local CI worktree: ${removal.refused.reason} (${removal.refused.path})`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
await rm(path.dirname(resolved), { recursive: true, force: true });
|
|
616
|
+
}
|
|
617
|
+
var DEFAULT_LOCAL_CI_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
618
|
+
async function runLocalCiVerificationCommand(command, cwd, options) {
|
|
619
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_LOCAL_CI_COMMAND_TIMEOUT_MS;
|
|
620
|
+
const env = createLocalCiTargetValidationEnv(process.env, {
|
|
621
|
+
trustedBaseSha: options?.trustedBaseSha,
|
|
622
|
+
validationProfile: options?.validationProfile ?? LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
623
|
+
gateRetry: options?.gateRetry
|
|
624
|
+
});
|
|
625
|
+
const setup = parseProjectDependencySetupCommand(command);
|
|
626
|
+
const groupGap = setup ? await describeCiPipDependencyGroupGap(setup, cwd, env) : void 0;
|
|
627
|
+
if (groupGap)
|
|
628
|
+
return { exitCode: 0, output: `Warning: skipped dependency group install: ${groupGap}` };
|
|
629
|
+
const result = setup ? await runVerificationArgv(setup.file, setup.args, {
|
|
630
|
+
cwd,
|
|
631
|
+
timeoutMs,
|
|
632
|
+
env,
|
|
633
|
+
// Argv already constrained by parseProjectDependencySetupCommand;
|
|
634
|
+
// verify safety rejects install verbs, so mark structured setup explicitly.
|
|
635
|
+
dependencySetup: true,
|
|
636
|
+
abortSignal: options?.signal
|
|
637
|
+
}) : await runVerificationCommand(command, {
|
|
638
|
+
cwd,
|
|
639
|
+
timeoutMs,
|
|
640
|
+
env,
|
|
641
|
+
abortSignal: options?.signal
|
|
642
|
+
});
|
|
643
|
+
const output = String(result.all ?? [result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
644
|
+
return {
|
|
645
|
+
exitCode: result.exitCode ?? 1,
|
|
646
|
+
output,
|
|
647
|
+
// A cancelled command's group was still alive after the bounded wait.
|
|
648
|
+
..."processGroupSurvived" in result ? { processGroupSurvived: true } : {}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async function describeCiPipDependencyGroupGap(setup, cwd, env) {
|
|
652
|
+
if (!isPipDependencyGroupInstall(setup.args)) return void 0;
|
|
653
|
+
const envRoot = selectPythonEnv(cwd)?.envRoot;
|
|
654
|
+
let version = envRoot ? readVenvPipVersion(envRoot) : void 0;
|
|
655
|
+
if (!envRoot) {
|
|
656
|
+
const probe = await execa2(setup.file, ["-I", "-m", "pip", "--version"], {
|
|
657
|
+
cwd,
|
|
658
|
+
env,
|
|
659
|
+
extendEnv: false,
|
|
660
|
+
reject: false,
|
|
661
|
+
timeout: 15e3
|
|
662
|
+
}).catch(() => void 0);
|
|
663
|
+
version = parsePipVersionOutput(String(probe?.stdout ?? ""));
|
|
664
|
+
}
|
|
665
|
+
return describePipDependencyGroupGap(version, setup.args[4] ?? "");
|
|
666
|
+
}
|
|
667
|
+
async function resolveCanonicalMainRepoRoot(cwd) {
|
|
668
|
+
const result = await execGit(["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
669
|
+
cwd
|
|
670
|
+
});
|
|
671
|
+
if (result.exitCode !== 0) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Unable to resolve canonical repository root from ${cwd}: ${result.stderr || result.stdout}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const commonDir = result.stdout.trim();
|
|
677
|
+
if (!commonDir) {
|
|
678
|
+
throw new Error(`git rev-parse returned an empty --git-common-dir for ${cwd}`);
|
|
679
|
+
}
|
|
680
|
+
const candidate = path.dirname(commonDir);
|
|
681
|
+
const validation = await execGit(["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
682
|
+
cwd: candidate
|
|
683
|
+
});
|
|
684
|
+
if (validation.exitCode !== 0) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Resolved candidate repository root ${candidate} (from ${cwd}) is not a valid working tree: ${validation.stderr || validation.stdout}`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const topLevel = validation.stdout.trim();
|
|
690
|
+
if (!topLevel || path.resolve(topLevel) !== path.resolve(candidate)) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`Resolved candidate repository root ${candidate} (from ${cwd}) does not match its own working-tree root ${topLevel}; unsupported git topology (e.g. separate-git-dir or submodule).`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return candidate;
|
|
696
|
+
}
|
|
697
|
+
function keepMacAwakeForCurrentProcess() {
|
|
698
|
+
if (process.platform !== "darwin") return;
|
|
699
|
+
const child = execa2("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
700
|
+
reject: false,
|
|
701
|
+
stdio: "ignore"
|
|
702
|
+
});
|
|
703
|
+
child.unref();
|
|
704
|
+
void child.catch(() => void 0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/core/ci/local-ci-authority.ts
|
|
708
|
+
import { setTimeout as delay3 } from "timers/promises";
|
|
709
|
+
|
|
710
|
+
// src/core/ci/local-ci-command-entrypoint.ts
|
|
711
|
+
import { lstatSync, readFileSync } from "fs";
|
|
712
|
+
import * as path2 from "path";
|
|
713
|
+
|
|
714
|
+
// src/core/ci/local-ci-trusted-launchers.ts
|
|
715
|
+
var NAMED_CONFIG_OPTIONS = /* @__PURE__ */ new Set([
|
|
716
|
+
"--config",
|
|
717
|
+
"-c",
|
|
718
|
+
"--config-file",
|
|
719
|
+
"--configuration",
|
|
720
|
+
"--rcfile",
|
|
721
|
+
"--project",
|
|
722
|
+
"--settings"
|
|
723
|
+
]);
|
|
724
|
+
var BUILD_DEFINITION_LANGUAGES = /* @__PURE__ */ new Set(["dotnet", "native", "r"]);
|
|
725
|
+
var BUILD_DEFINITION_HEADS = /* @__PURE__ */ new Set(["dbt"]);
|
|
726
|
+
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpx", "bunx"]);
|
|
727
|
+
var EXEC_VERB_RUNNERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn"]);
|
|
728
|
+
function tscIndex(tokens) {
|
|
729
|
+
const [head = "", verb = ""] = tokens;
|
|
730
|
+
let index = 0;
|
|
731
|
+
if (PACKAGE_RUNNERS.has(head)) index = 1;
|
|
732
|
+
else if (EXEC_VERB_RUNNERS.has(head) && verb === "exec" || head === "bun" && verb === "x") {
|
|
733
|
+
index = 2;
|
|
734
|
+
}
|
|
735
|
+
if (index > 0) while ((tokens[index] ?? "").startsWith("-")) index += 1;
|
|
736
|
+
return (tokens[index] ?? "").split("/").pop() === "tsc" ? index : -1;
|
|
737
|
+
}
|
|
738
|
+
var EXEC_WRAPPERS = {
|
|
739
|
+
bundle: { verb: "exec", strict: true, pins: [".bundle/config"] },
|
|
740
|
+
pipenv: { verb: "run", strict: true, pins: [] },
|
|
741
|
+
poetry: { verb: "run", strict: false, pins: [] },
|
|
742
|
+
uv: { verb: "run", strict: false, pins: [] },
|
|
743
|
+
pdm: { verb: "run", strict: false, pins: [] },
|
|
744
|
+
hatch: { verb: "run", strict: false, pins: [] }
|
|
745
|
+
};
|
|
746
|
+
function unwrapExecWrapper(head, tokens) {
|
|
747
|
+
const wrapper = EXEC_WRAPPERS[head];
|
|
748
|
+
if (!wrapper || (tokens[1] ?? "").toLowerCase() !== wrapper.verb) return null;
|
|
749
|
+
let index = 2;
|
|
750
|
+
while (index < tokens.length && (tokens[index] ?? "").startsWith("-")) index += 1;
|
|
751
|
+
const inner = tokens.slice(index);
|
|
752
|
+
return inner.length === 0 ? null : { tokens: inner, strict: wrapper.strict, pins: wrapper.pins };
|
|
753
|
+
}
|
|
754
|
+
function namedConfigOperands(tokens) {
|
|
755
|
+
const operands = [];
|
|
756
|
+
const tsc = tscIndex(tokens);
|
|
757
|
+
const checkShorthand = isPrettierInvocation(tokens);
|
|
758
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
759
|
+
const token = tokens[index] ?? "";
|
|
760
|
+
const eq = token.indexOf("=");
|
|
761
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
762
|
+
const tscProject = name === "-p" && tsc >= 0 && index > tsc;
|
|
763
|
+
if (!NAMED_CONFIG_OPTIONS.has(name) && !tscProject) continue;
|
|
764
|
+
if (checkShorthand && name === "-c") continue;
|
|
765
|
+
if (eq !== -1) operands.push(token.slice(eq + 1));
|
|
766
|
+
else if (tokens[index + 1] !== void 0) operands.push(tokens[index + 1] ?? "");
|
|
767
|
+
}
|
|
768
|
+
return operands;
|
|
769
|
+
}
|
|
770
|
+
function recognizeTrustedLauncher(head, tokens) {
|
|
771
|
+
const wrapped = unwrapExecWrapper(head, tokens);
|
|
772
|
+
if (wrapped) {
|
|
773
|
+
return {
|
|
774
|
+
pins: [...wrapped.pins, ...namedConfigOperands(tokens)],
|
|
775
|
+
nested: { tokens: wrapped.tokens, strict: wrapped.strict }
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const command = tokens.join(" ");
|
|
779
|
+
if (parseProjectDependencySetupCommand(command)) return { pins: [] };
|
|
780
|
+
const described = describeVerificationCommand(command);
|
|
781
|
+
if (described.language === "unknown" || described.kind === "unknown") return null;
|
|
782
|
+
if (BUILD_DEFINITION_LANGUAGES.has(described.language) || BUILD_DEFINITION_HEADS.has(head)) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
return { pins: namedConfigOperands(tokens) };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/core/ci/local-ci-version-only-lockfile.ts
|
|
789
|
+
var SUPPORTED_LOCKFILE_VERSION = 3;
|
|
790
|
+
function isPlainObject(value) {
|
|
791
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
792
|
+
}
|
|
793
|
+
function deepEqual(left, right) {
|
|
794
|
+
if (left === right) return true;
|
|
795
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
796
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => deepEqual(item, right[index]));
|
|
797
|
+
}
|
|
798
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
799
|
+
const leftKeys = Object.keys(left);
|
|
800
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
function stripLockfileVersions(text) {
|
|
805
|
+
let document;
|
|
806
|
+
try {
|
|
807
|
+
document = JSON.parse(text);
|
|
808
|
+
} catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
if (!isPlainObject(document) || typeof document.version !== "string") return null;
|
|
812
|
+
if (document.lockfileVersion !== SUPPORTED_LOCKFILE_VERSION) return null;
|
|
813
|
+
const packages = document.packages;
|
|
814
|
+
if (!isPlainObject(packages)) return null;
|
|
815
|
+
const rootPackage = packages[""];
|
|
816
|
+
if (!isPlainObject(rootPackage) || typeof rootPackage.version !== "string") return null;
|
|
817
|
+
const { version: _version, ...rest } = document;
|
|
818
|
+
const { version: _rootVersion, ...rootWithoutVersion } = rootPackage;
|
|
819
|
+
return { ...rest, packages: { ...packages, "": rootWithoutVersion } };
|
|
820
|
+
}
|
|
821
|
+
function isVersionOnlyLockfileChange(baseText, candidateText) {
|
|
822
|
+
const base = stripLockfileVersions(baseText);
|
|
823
|
+
const candidate = stripLockfileVersions(candidateText);
|
|
824
|
+
return base !== null && candidate !== null && deepEqual(base, candidate);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/core/ci/local-ci-command-entrypoint.ts
|
|
828
|
+
var LOCKFILE_PATH = "package-lock.json";
|
|
829
|
+
var PACKAGE_SCRIPT_VERBS = /* @__PURE__ */ new Set(["run", "run-script", "test", "start", "exec", "task"]);
|
|
830
|
+
var PACKAGE_MANAGER_HEADS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun", "deno"]);
|
|
831
|
+
var INTERPRETER_HEADS = /* @__PURE__ */ new Set([
|
|
832
|
+
"node",
|
|
833
|
+
"nodejs",
|
|
834
|
+
"deno",
|
|
835
|
+
"bun",
|
|
836
|
+
"tsx",
|
|
837
|
+
"ts-node",
|
|
838
|
+
"python",
|
|
839
|
+
"python2",
|
|
840
|
+
"python3",
|
|
841
|
+
"pypy",
|
|
842
|
+
"pypy3",
|
|
843
|
+
"ruby",
|
|
844
|
+
"perl",
|
|
845
|
+
"php",
|
|
846
|
+
"rscript",
|
|
847
|
+
"lua",
|
|
848
|
+
"luajit",
|
|
849
|
+
"sh",
|
|
850
|
+
"bash",
|
|
851
|
+
"zsh",
|
|
852
|
+
"dash",
|
|
853
|
+
"ksh",
|
|
854
|
+
"fish",
|
|
855
|
+
"pwsh",
|
|
856
|
+
"powershell"
|
|
857
|
+
]);
|
|
858
|
+
var SHELL_INTERPRETER_HEADS = /* @__PURE__ */ new Set([
|
|
859
|
+
"sh",
|
|
860
|
+
"bash",
|
|
861
|
+
"zsh",
|
|
862
|
+
"dash",
|
|
863
|
+
"ksh",
|
|
864
|
+
"fish",
|
|
865
|
+
"pwsh",
|
|
866
|
+
"powershell"
|
|
867
|
+
]);
|
|
868
|
+
var MAX_REPORTED_PATHS = 3;
|
|
869
|
+
function commandSegments(command) {
|
|
870
|
+
return command.split(/\|\||&&|[;\n|]/).map((segment) => segment.trim()).filter(Boolean);
|
|
871
|
+
}
|
|
872
|
+
function headBasename(token) {
|
|
873
|
+
const lowered = token.toLowerCase();
|
|
874
|
+
return lowered.includes("/") ? lowered.split("/").pop() ?? lowered : lowered;
|
|
875
|
+
}
|
|
876
|
+
var ENVIRONMENT_WRAPPER_HEADS = /* @__PURE__ */ new Set(["env", "cross-env"]);
|
|
877
|
+
var ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
878
|
+
var NODE_VALUE_OPTIONS = [
|
|
879
|
+
"--require",
|
|
880
|
+
"-r",
|
|
881
|
+
"--import",
|
|
882
|
+
"--loader",
|
|
883
|
+
"--experimental-loader",
|
|
884
|
+
"--env-file",
|
|
885
|
+
"--env-file-if-exists"
|
|
886
|
+
];
|
|
887
|
+
var INTERPRETER_VALUE_OPTIONS = {
|
|
888
|
+
node: new Set(NODE_VALUE_OPTIONS),
|
|
889
|
+
nodejs: new Set(NODE_VALUE_OPTIONS),
|
|
890
|
+
"ts-node": /* @__PURE__ */ new Set([...NODE_VALUE_OPTIONS, "-P", "--project"]),
|
|
891
|
+
tsx: /* @__PURE__ */ new Set([...NODE_VALUE_OPTIONS, "--tsconfig"]),
|
|
892
|
+
deno: /* @__PURE__ */ new Set(["-c", "--config", "--import-map", "--lock", "--cert"]),
|
|
893
|
+
bun: /* @__PURE__ */ new Set(["-c", "--config", "-r", "--preload", "--tsconfig-override", "--env-file"]),
|
|
894
|
+
ruby: /* @__PURE__ */ new Set(["-I", "-r", "-C"]),
|
|
895
|
+
perl: /* @__PURE__ */ new Set(["-I"]),
|
|
896
|
+
php: /* @__PURE__ */ new Set(["-c", "-d"])
|
|
897
|
+
};
|
|
898
|
+
var PACKAGE_MANAGER_PASSIVE_OPTIONS = /* @__PURE__ */ new Set([
|
|
899
|
+
"-s",
|
|
900
|
+
"--silent",
|
|
901
|
+
"-q",
|
|
902
|
+
"--quiet",
|
|
903
|
+
"--verbose",
|
|
904
|
+
"--if-present"
|
|
905
|
+
]);
|
|
906
|
+
var NPM_MANIFEST_SELECTING_OPTIONS = /* @__PURE__ */ new Set([
|
|
907
|
+
"-w",
|
|
908
|
+
"--workspace",
|
|
909
|
+
"-ws",
|
|
910
|
+
"--ws",
|
|
911
|
+
"--workspaces",
|
|
912
|
+
"-C",
|
|
913
|
+
"--prefix"
|
|
914
|
+
]);
|
|
915
|
+
var MAX_ENVIRONMENT_UNWRAP_DEPTH = 8;
|
|
916
|
+
function isEnvAssignmentToken(token) {
|
|
917
|
+
return ENV_ASSIGNMENT.test(token);
|
|
918
|
+
}
|
|
919
|
+
function skipLeadingEnvAssignments(tokens) {
|
|
920
|
+
let index = 0;
|
|
921
|
+
while (index < tokens.length && isEnvAssignmentToken(tokens[index] ?? "")) {
|
|
922
|
+
index += 1;
|
|
923
|
+
}
|
|
924
|
+
return index === 0 ? tokens : tokens.slice(index);
|
|
925
|
+
}
|
|
926
|
+
function unwrapSingleEnvironmentWrapper(tokens) {
|
|
927
|
+
if (!ENVIRONMENT_WRAPPER_HEADS.has(headBasename(tokens[0] ?? ""))) return null;
|
|
928
|
+
let index = 1;
|
|
929
|
+
while (index < tokens.length) {
|
|
930
|
+
const token = tokens[index] ?? "";
|
|
931
|
+
if (token.startsWith("-") || isEnvAssignmentToken(token)) {
|
|
932
|
+
index += 1;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
return tokens.slice(index);
|
|
938
|
+
}
|
|
939
|
+
function unwrapEnvironmentLauncher(tokens) {
|
|
940
|
+
let remaining = skipLeadingEnvAssignments(tokens);
|
|
941
|
+
for (let depth = 0; depth < MAX_ENVIRONMENT_UNWRAP_DEPTH; depth += 1) {
|
|
942
|
+
const unwrapped = unwrapSingleEnvironmentWrapper(remaining);
|
|
943
|
+
if (unwrapped === null) return remaining;
|
|
944
|
+
remaining = skipLeadingEnvAssignments(unwrapped);
|
|
945
|
+
}
|
|
946
|
+
return remaining;
|
|
947
|
+
}
|
|
948
|
+
function isModeledLauncherHead(head, firstToken) {
|
|
949
|
+
return Boolean(
|
|
950
|
+
launcherManifests(head) || INTERPRETER_HEADS.has(head) || operandArtifactPath(firstToken)
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
function interpreterValueOperand(head, token) {
|
|
954
|
+
const eq = token.indexOf("=");
|
|
955
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
956
|
+
if (!INTERPRETER_VALUE_OPTIONS[head]?.has(name)) return void 0;
|
|
957
|
+
if (eq !== -1) return { operand: token.slice(eq + 1), consumesNext: false };
|
|
958
|
+
return { consumesNext: true };
|
|
959
|
+
}
|
|
960
|
+
function collectInterpreterArtifacts(head, tokens, into, onAmbiguousProgram) {
|
|
961
|
+
let afterUnlistedOption = false;
|
|
962
|
+
let firstCandidate = null;
|
|
963
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
964
|
+
const token = tokens[index] ?? "";
|
|
965
|
+
const valueOption = interpreterValueOperand(head, token);
|
|
966
|
+
if (valueOption) {
|
|
967
|
+
const operand = valueOption.consumesNext ? tokens[index + 1] ?? "" : valueOption.operand ?? "";
|
|
968
|
+
if (valueOption.consumesNext) index += 1;
|
|
969
|
+
const artifact = operandArtifactPath(operand);
|
|
970
|
+
if (artifact) into.add(artifact);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (token.startsWith("-")) {
|
|
974
|
+
if (!token.includes("=")) afterUnlistedOption = true;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const repoPath = operandRepoPath(token);
|
|
978
|
+
if (repoPath) {
|
|
979
|
+
into.add(foldArtifactPath(repoPath));
|
|
980
|
+
if (firstCandidate !== null) onAmbiguousProgram?.();
|
|
981
|
+
if (!afterUnlistedOption) return firstCandidate ?? repoPath;
|
|
982
|
+
firstCandidate ??= repoPath;
|
|
983
|
+
}
|
|
984
|
+
afterUnlistedOption = false;
|
|
985
|
+
}
|
|
986
|
+
return firstCandidate;
|
|
987
|
+
}
|
|
988
|
+
function operandRepoPath(token) {
|
|
989
|
+
const unquoted = token.replace(/^['"]|['"]$/g, "");
|
|
990
|
+
if (!unquoted || unquoted.startsWith("-")) return null;
|
|
991
|
+
if (unquoted.includes("://")) return null;
|
|
992
|
+
const repoRelative = unquoted.replace(/^\$\{?REPO_ROOT\}?\//i, "");
|
|
993
|
+
if (!repoRelative.includes("/") && !/\.[a-z0-9]{1,8}$/i.test(repoRelative)) return null;
|
|
994
|
+
return normalizeRepoRelativePath(repoRelative);
|
|
995
|
+
}
|
|
996
|
+
function foldArtifactPath(repoPath) {
|
|
997
|
+
return repoPath.toLowerCase();
|
|
998
|
+
}
|
|
999
|
+
function operandArtifactPath(token) {
|
|
1000
|
+
const repoPath = operandRepoPath(token);
|
|
1001
|
+
return repoPath === null ? null : foldArtifactPath(repoPath);
|
|
1002
|
+
}
|
|
1003
|
+
function skipPassivePackageManagerOptions(tokens, start) {
|
|
1004
|
+
let index = start;
|
|
1005
|
+
while (index < tokens.length && (tokens[index] ?? "").startsWith("-")) {
|
|
1006
|
+
if (!PACKAGE_MANAGER_PASSIVE_OPTIONS.has((tokens[index] ?? "").toLowerCase())) return -1;
|
|
1007
|
+
if (/^(true|false)$/i.test(tokens[index + 1] ?? "")) return -1;
|
|
1008
|
+
index += 1;
|
|
1009
|
+
}
|
|
1010
|
+
return index;
|
|
1011
|
+
}
|
|
1012
|
+
function resolvePackageManagerInvocation(head, tokens) {
|
|
1013
|
+
if (!PACKAGE_MANAGER_HEADS.has(head)) return null;
|
|
1014
|
+
const unresolved = { verb: "", scriptName: null, unresolved: true };
|
|
1015
|
+
const verbIndex = skipPassivePackageManagerOptions(tokens, 1);
|
|
1016
|
+
if (verbIndex === -1) {
|
|
1017
|
+
return tokens.length === 2 ? { verb: "", scriptName: null, unresolved: false } : unresolved;
|
|
1018
|
+
}
|
|
1019
|
+
const verb = (tokens[verbIndex] ?? "").toLowerCase();
|
|
1020
|
+
const scriptAfterVerb = () => {
|
|
1021
|
+
const nameIndex = skipPassivePackageManagerOptions(tokens, verbIndex + 1);
|
|
1022
|
+
const name = nameIndex === -1 ? void 0 : tokens[nameIndex];
|
|
1023
|
+
if (!name) return unresolved;
|
|
1024
|
+
return { verb, scriptName: name.replace(/^['"]|['"]$/g, ""), unresolved: false };
|
|
1025
|
+
};
|
|
1026
|
+
let invocation = { verb, scriptName: null, unresolved: false };
|
|
1027
|
+
if (head === "npm" || head === "pnpm") {
|
|
1028
|
+
if (verb === "run" || verb === "run-script") invocation = scriptAfterVerb();
|
|
1029
|
+
else if (verb === "test" || verb === "start") invocation.scriptName = verb;
|
|
1030
|
+
} else if (head === "yarn" || head === "bun") {
|
|
1031
|
+
if (verb === "workspace" || verb === "workspaces") return unresolved;
|
|
1032
|
+
if (verb === "run") invocation = scriptAfterVerb();
|
|
1033
|
+
else if (verb && !["install", "add", "remove"].includes(verb)) invocation.scriptName = verb;
|
|
1034
|
+
}
|
|
1035
|
+
if (head === "npm" && invocation.scriptName !== null) {
|
|
1036
|
+
for (const token of tokens.slice(verbIndex + 1)) {
|
|
1037
|
+
if (token === "--") break;
|
|
1038
|
+
const name = token.split("=")[0] ?? "";
|
|
1039
|
+
if (NPM_MANIFEST_SELECTING_OPTIONS.has(name)) return unresolved;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return invocation;
|
|
1043
|
+
}
|
|
1044
|
+
function readPackageScripts(repoRoot) {
|
|
1045
|
+
try {
|
|
1046
|
+
const parsed = JSON.parse(readFileSync(path2.join(repoRoot, "package.json"), "utf8"));
|
|
1047
|
+
if (!parsed.scripts || typeof parsed.scripts !== "object" || Array.isArray(parsed.scripts)) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
return Object.fromEntries(
|
|
1051
|
+
Object.entries(parsed.scripts).filter(
|
|
1052
|
+
(entry) => typeof entry[1] === "string"
|
|
1053
|
+
)
|
|
1054
|
+
);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function entrypointSidecarPath(artifact) {
|
|
1060
|
+
const extensionIndex = artifact.lastIndexOf(".");
|
|
1061
|
+
const stem = extensionIndex > artifact.lastIndexOf("/") ? artifact.slice(0, extensionIndex) : artifact;
|
|
1062
|
+
return `${stem}.entrypoints.json`;
|
|
1063
|
+
}
|
|
1064
|
+
function readEntrypointSidecar(trustedRoot, artifact, unmodeledOut) {
|
|
1065
|
+
const sidecarPath = entrypointSidecarPath(artifact);
|
|
1066
|
+
try {
|
|
1067
|
+
const parsed = JSON.parse(readFileSync(path2.join(trustedRoot, sidecarPath), "utf8"));
|
|
1068
|
+
if (!Array.isArray(parsed.packageScripts) || parsed.packageScripts.some((value) => typeof value !== "string" || value.trim().length === 0)) {
|
|
1069
|
+
unmodeledOut?.push(`invalid trusted entrypoint sidecar: ${sidecarPath}`);
|
|
1070
|
+
return { path: sidecarPath, packageScripts: [] };
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
path: sidecarPath,
|
|
1074
|
+
packageScripts: [...new Set(parsed.packageScripts.map((value) => value.trim()))]
|
|
1075
|
+
};
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
1078
|
+
if (code === "ENOENT") return null;
|
|
1079
|
+
unmodeledOut?.push(`invalid trusted entrypoint sidecar: ${sidecarPath}`);
|
|
1080
|
+
return { path: sidecarPath, packageScripts: [] };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function bindNamedOperands(operands, into) {
|
|
1084
|
+
for (const operand of operands) {
|
|
1085
|
+
const artifact = operandArtifactPath(operand);
|
|
1086
|
+
if (artifact) into.add(artifact);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function collectNestedLauncher(nested, into, trustedRoot, visitedScripts, context) {
|
|
1090
|
+
collectSegmentEntrypoints(
|
|
1091
|
+
nested.tokens.join(" "),
|
|
1092
|
+
into,
|
|
1093
|
+
trustedRoot,
|
|
1094
|
+
visitedScripts,
|
|
1095
|
+
context.visitedShellArtifacts,
|
|
1096
|
+
context.unmodeledOut,
|
|
1097
|
+
false
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
function collectSegmentEntrypoints(segment, into, trustedRoot, visitedScripts = /* @__PURE__ */ new Set(), visitedShellArtifacts = /* @__PURE__ */ new Set(), unmodeledOut, fromTrustedShellArtifact = false) {
|
|
1101
|
+
const tokens = unwrapEnvironmentLauncher(segment.split(/\s+/).filter(Boolean));
|
|
1102
|
+
const head = headBasename(tokens[0] ?? "");
|
|
1103
|
+
if (!head) return;
|
|
1104
|
+
if (!isModeledLauncherHead(head, tokens[0] ?? "")) {
|
|
1105
|
+
if (!fromTrustedShellArtifact) {
|
|
1106
|
+
const recognized = recognizeTrustedLauncher(head, tokens);
|
|
1107
|
+
if (recognized) {
|
|
1108
|
+
bindNamedOperands(recognized.pins, into);
|
|
1109
|
+
if (recognized.nested) {
|
|
1110
|
+
collectNestedLauncher(recognized.nested, into, trustedRoot, visitedScripts, {
|
|
1111
|
+
visitedShellArtifacts,
|
|
1112
|
+
unmodeledOut: recognized.nested.strict ? unmodeledOut : void 0
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
unmodeledOut?.push(segment);
|
|
1118
|
+
} else {
|
|
1119
|
+
for (const token of tokens.slice(1)) {
|
|
1120
|
+
const artifact = operandArtifactPath(token);
|
|
1121
|
+
if (artifact) into.add(artifact);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const packageInvocation = resolvePackageManagerInvocation(head, tokens);
|
|
1127
|
+
if (packageInvocation?.unresolved) unmodeledOut?.push(segment);
|
|
1128
|
+
const manifests = launcherManifests(head);
|
|
1129
|
+
if (manifests) {
|
|
1130
|
+
const verb = packageInvocation?.verb ?? "";
|
|
1131
|
+
const pinned = packageInvocation ? packageInvocation.unresolved || PACKAGE_SCRIPT_VERBS.has(verb) || head === "yarn" && verb.length > 0 ? manifests.definitions : [] : pinnedManifestFiles(manifests, tokens);
|
|
1132
|
+
const tableScript = packageInvocation ? null : referencedTableScript(head, tokens);
|
|
1133
|
+
for (const manifest of pinned) {
|
|
1134
|
+
const normalized = normalizeAuthorityPath(manifest);
|
|
1135
|
+
if (!normalized) continue;
|
|
1136
|
+
const scriptName2 = normalized === normalizeAuthorityPath(tableScript?.file ?? "") ? tableScript?.name : void 0;
|
|
1137
|
+
into.add(scriptName2 === void 0 ? normalized : scopedPinKey(normalized, head, scriptName2));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (!INTERPRETER_HEADS.has(head) && !fromTrustedShellArtifact) {
|
|
1141
|
+
bindNamedOperands(namedConfigOperands(tokens), into);
|
|
1142
|
+
const wrapped = unwrapExecWrapper(head, tokens);
|
|
1143
|
+
if (wrapped) {
|
|
1144
|
+
collectNestedLauncher(wrapped, into, trustedRoot, visitedScripts, {
|
|
1145
|
+
visitedShellArtifacts,
|
|
1146
|
+
unmodeledOut: wrapped.strict ? unmodeledOut : void 0
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const interpreterArtifact = INTERPRETER_HEADS.has(head) ? collectInterpreterArtifacts(head, tokens, into, () => unmodeledOut?.push(segment)) : null;
|
|
1151
|
+
const directArtifact = operandRepoPath(tokens[0] ?? "");
|
|
1152
|
+
if (directArtifact) into.add(foldArtifactPath(directArtifact));
|
|
1153
|
+
const executableArtifact = interpreterArtifact ?? directArtifact;
|
|
1154
|
+
if (trustedRoot && executableArtifact) {
|
|
1155
|
+
const sidecar = readEntrypointSidecar(trustedRoot, executableArtifact, unmodeledOut);
|
|
1156
|
+
if (sidecar) {
|
|
1157
|
+
into.add(foldArtifactPath(sidecar.path));
|
|
1158
|
+
if (sidecar.packageScripts.length > 0) {
|
|
1159
|
+
for (const manifest of NODE_PACKAGE_MANAGER_MANIFESTS) {
|
|
1160
|
+
const normalized = normalizeAuthorityPath(manifest);
|
|
1161
|
+
if (normalized) into.add(normalized);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const shellArtifact = SHELL_INTERPRETER_HEADS.has(head) ? interpreterArtifact : directArtifact && foldArtifactPath(directArtifact).endsWith(".sh") ? directArtifact : null;
|
|
1167
|
+
if (trustedRoot && shellArtifact && !visitedShellArtifacts.has(shellArtifact)) {
|
|
1168
|
+
try {
|
|
1169
|
+
const contents = readFileSync(path2.join(trustedRoot, shellArtifact), "utf8");
|
|
1170
|
+
visitedShellArtifacts.add(shellArtifact);
|
|
1171
|
+
for (const nested of commandSegments(contents)) {
|
|
1172
|
+
collectSegmentEntrypoints(
|
|
1173
|
+
nested,
|
|
1174
|
+
into,
|
|
1175
|
+
trustedRoot,
|
|
1176
|
+
visitedScripts,
|
|
1177
|
+
visitedShellArtifacts,
|
|
1178
|
+
unmodeledOut,
|
|
1179
|
+
true
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const scriptName = packageInvocation?.scriptName ?? null;
|
|
1186
|
+
if (trustedRoot && scriptName) {
|
|
1187
|
+
const scripts = readPackageScripts(trustedRoot);
|
|
1188
|
+
for (const lifecycleName of [`pre${scriptName}`, scriptName, `post${scriptName}`]) {
|
|
1189
|
+
const script = scripts?.[lifecycleName];
|
|
1190
|
+
if (!script || visitedScripts.has(lifecycleName)) continue;
|
|
1191
|
+
visitedScripts.add(lifecycleName);
|
|
1192
|
+
for (const nested of commandSegments(script)) {
|
|
1193
|
+
collectSegmentEntrypoints(
|
|
1194
|
+
nested,
|
|
1195
|
+
into,
|
|
1196
|
+
trustedRoot,
|
|
1197
|
+
visitedScripts,
|
|
1198
|
+
visitedShellArtifacts,
|
|
1199
|
+
unmodeledOut,
|
|
1200
|
+
false
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function collectEntrypointPins(commands, trustedRoot) {
|
|
1207
|
+
const raw = /* @__PURE__ */ new Set();
|
|
1208
|
+
for (const command of commands) {
|
|
1209
|
+
for (const segment of commandSegments(command)) {
|
|
1210
|
+
collectSegmentEntrypoints(segment, raw, trustedRoot);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1214
|
+
const whole = /* @__PURE__ */ new Set();
|
|
1215
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const key of raw) {
|
|
1217
|
+
const pin = parseScopedPin(key);
|
|
1218
|
+
if (!pin) {
|
|
1219
|
+
paths.add(key);
|
|
1220
|
+
whole.add(key);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
paths.add(pin.file);
|
|
1224
|
+
scoped.set(pin.file, [...scoped.get(pin.file) ?? [], { head: pin.head, name: pin.name }]);
|
|
1225
|
+
}
|
|
1226
|
+
return { paths: [...paths].sort(), whole, scoped };
|
|
1227
|
+
}
|
|
1228
|
+
function referencedPackageScripts(commands, trustedRoot) {
|
|
1229
|
+
const scripts = readPackageScripts(trustedRoot) ?? {};
|
|
1230
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
1231
|
+
const visitedShellArtifacts = /* @__PURE__ */ new Set();
|
|
1232
|
+
const visitName = (name) => {
|
|
1233
|
+
for (const lifecycleName of [`pre${name}`, name, `post${name}`]) {
|
|
1234
|
+
if (referenced.has(lifecycleName)) continue;
|
|
1235
|
+
const value = scripts[lifecycleName];
|
|
1236
|
+
referenced.set(lifecycleName, value);
|
|
1237
|
+
if (value !== void 0) visit(value);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
const visit = (command) => {
|
|
1241
|
+
for (const segment of commandSegments(command)) {
|
|
1242
|
+
const tokens = unwrapEnvironmentLauncher(segment.split(/\s+/).filter(Boolean));
|
|
1243
|
+
const head = headBasename(tokens[0] ?? "");
|
|
1244
|
+
const name = resolvePackageManagerInvocation(head, tokens)?.scriptName;
|
|
1245
|
+
if (name) visitName(name);
|
|
1246
|
+
const wrapped = unwrapExecWrapper(head, tokens);
|
|
1247
|
+
if (wrapped) visit(wrapped.tokens.join(" "));
|
|
1248
|
+
const artifact = INTERPRETER_HEADS.has(head) ? collectInterpreterArtifacts(head, tokens, /* @__PURE__ */ new Set()) : operandRepoPath(tokens[0] ?? "");
|
|
1249
|
+
if (artifact) {
|
|
1250
|
+
const sidecar = readEntrypointSidecar(trustedRoot, artifact);
|
|
1251
|
+
for (const declaredName of sidecar?.packageScripts ?? []) {
|
|
1252
|
+
visitName(declaredName);
|
|
1253
|
+
}
|
|
1254
|
+
const shellArtifact = (SHELL_INTERPRETER_HEADS.has(head) ? artifact : void 0) ?? (foldArtifactPath(artifact).endsWith(".sh") ? artifact : void 0);
|
|
1255
|
+
if (shellArtifact && !visitedShellArtifacts.has(shellArtifact)) {
|
|
1256
|
+
visitedShellArtifacts.add(shellArtifact);
|
|
1257
|
+
try {
|
|
1258
|
+
visit(readFileSync(path2.join(trustedRoot, shellArtifact), "utf8"));
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
for (const command of commands) visit(command);
|
|
1266
|
+
return referenced;
|
|
1267
|
+
}
|
|
1268
|
+
function lockfileChangeIsVersionOnly(baseRoot, candidateRoot) {
|
|
1269
|
+
try {
|
|
1270
|
+
const basePath = path2.join(baseRoot, LOCKFILE_PATH);
|
|
1271
|
+
const candidatePath = path2.join(candidateRoot, LOCKFILE_PATH);
|
|
1272
|
+
const baseStat = lstatSync(basePath);
|
|
1273
|
+
const candidateStat = lstatSync(candidatePath);
|
|
1274
|
+
if (!baseStat.isFile() || !candidateStat.isFile()) return false;
|
|
1275
|
+
if ((baseStat.mode & 73) !== (candidateStat.mode & 73)) return false;
|
|
1276
|
+
return isVersionOnlyLockfileChange(
|
|
1277
|
+
readFileSync(basePath, "utf8"),
|
|
1278
|
+
readFileSync(candidatePath, "utf8")
|
|
1279
|
+
);
|
|
1280
|
+
} catch {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function repoFileEquals(baseRoot, candidateRoot, relativePath) {
|
|
1285
|
+
try {
|
|
1286
|
+
return readFileSync(path2.join(baseRoot, relativePath)).equals(
|
|
1287
|
+
readFileSync(path2.join(candidateRoot, relativePath))
|
|
1288
|
+
);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function unmodeledSegments(commands, trustedRoot) {
|
|
1294
|
+
const unmodeled = [];
|
|
1295
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1296
|
+
for (const command of commands) {
|
|
1297
|
+
for (const segment of commandSegments(command)) {
|
|
1298
|
+
collectSegmentEntrypoints(segment, paths, trustedRoot, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), unmodeled);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return unmodeled;
|
|
1302
|
+
}
|
|
1303
|
+
function namedScriptsUnchanged(named, trustedRoot, candidateRoot) {
|
|
1304
|
+
const heads = new Set(named.map((script) => script.head));
|
|
1305
|
+
return [...heads].every(
|
|
1306
|
+
(head) => scriptTableUnchanged(
|
|
1307
|
+
head,
|
|
1308
|
+
named.filter((script) => script.head === head).map((script) => script.name),
|
|
1309
|
+
trustedRoot,
|
|
1310
|
+
candidateRoot
|
|
1311
|
+
)
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
function checkTrustedCommandEntrypoints(input) {
|
|
1315
|
+
const pins = collectEntrypointPins(input.commands, input.trustedRoot);
|
|
1316
|
+
const entrypointPaths = pins.paths;
|
|
1317
|
+
const unmodeled = unmodeledSegments(input.commands, input.trustedRoot);
|
|
1318
|
+
const approved = input.candidateSha !== void 0 && input.approvedCandidateSha?.toLowerCase() === input.candidateSha.toLowerCase();
|
|
1319
|
+
if (entrypointPaths.length === 0) {
|
|
1320
|
+
if (unmodeled.length === 0) return { entrypointPaths };
|
|
1321
|
+
return approved ? { entrypointPaths, approvedBy: "BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA" } : {
|
|
1322
|
+
entrypointPaths,
|
|
1323
|
+
blockedReason: `trusted verification launcher is not modeled: ${unmodeled[0]}`
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
if (!input.changedFilesComplete) {
|
|
1327
|
+
return {
|
|
1328
|
+
entrypointPaths,
|
|
1329
|
+
blockedReason: "candidate change set is incomplete, so no entrypoint can be cleared"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const redefined = [];
|
|
1333
|
+
const unnormalized = [];
|
|
1334
|
+
const trackedPaths = /* @__PURE__ */ new Map();
|
|
1335
|
+
for (const filePath of input.changedFiles) {
|
|
1336
|
+
const folded = normalizeAuthorityPath(filePath);
|
|
1337
|
+
if (folded === null) {
|
|
1338
|
+
unnormalized.push(filePath);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const spellings = trackedPaths.get(folded) ?? [];
|
|
1342
|
+
if (!spellings.includes(filePath)) trackedPaths.set(folded, [...spellings, filePath]);
|
|
1343
|
+
if (entrypointPaths.includes(folded) && !redefined.includes(folded)) {
|
|
1344
|
+
if (folded === "package.json" && input.trustedRoot && input.candidateRoot) {
|
|
1345
|
+
const baseScripts = referencedPackageScripts(input.commands, input.trustedRoot);
|
|
1346
|
+
const candidateScripts = readPackageScripts(input.candidateRoot) ?? {};
|
|
1347
|
+
const changedDefinition = [...baseScripts].some(
|
|
1348
|
+
([name, value]) => candidateScripts[name] !== value
|
|
1349
|
+
);
|
|
1350
|
+
if (!changedDefinition) continue;
|
|
1351
|
+
} else if (input.trustedRoot && input.candidateRoot && !pins.whole.has(folded) && namedScriptsUnchanged(pins.scoped.get(folded) ?? [], input.trustedRoot, input.candidateRoot)) {
|
|
1352
|
+
continue;
|
|
1353
|
+
} else if (folded === LOCKFILE_PATH && input.trustedRoot && input.candidateRoot && lockfileChangeIsVersionOnly(input.trustedRoot, input.candidateRoot)) {
|
|
1354
|
+
continue;
|
|
1355
|
+
} else if (input.trustedRoot && input.candidateRoot && repoFileEquals(input.trustedRoot, input.candidateRoot, folded)) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
redefined.push(folded);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (redefined.length > 0 || unmodeled.length > 0) {
|
|
1362
|
+
const fingerprint = redefined.length > 0 && unmodeled.length === 0 && unnormalized.length === 0 && input.trustedRoot && input.candidateRoot ? computeEntrypointFingerprint({
|
|
1363
|
+
commands: input.commands,
|
|
1364
|
+
pins,
|
|
1365
|
+
redefined,
|
|
1366
|
+
trustedRoot: input.trustedRoot,
|
|
1367
|
+
candidateRoot: input.candidateRoot,
|
|
1368
|
+
trackedPaths,
|
|
1369
|
+
...redefined.includes("package.json") ? {
|
|
1370
|
+
packageScripts: {
|
|
1371
|
+
// Reachable in either tree, so a script the candidate newly
|
|
1372
|
+
// points the gate at is bound with its values.
|
|
1373
|
+
names: [
|
|
1374
|
+
.../* @__PURE__ */ new Set([
|
|
1375
|
+
...referencedPackageScripts(input.commands, input.trustedRoot).keys(),
|
|
1376
|
+
...referencedPackageScripts(input.commands, input.candidateRoot).keys()
|
|
1377
|
+
])
|
|
1378
|
+
],
|
|
1379
|
+
base: readPackageScripts(input.trustedRoot) ?? {},
|
|
1380
|
+
candidate: readPackageScripts(input.candidateRoot) ?? {}
|
|
1381
|
+
}
|
|
1382
|
+
} : {}
|
|
1383
|
+
}) : void 0;
|
|
1384
|
+
const withFingerprint = fingerprint === void 0 ? {} : { entrypointFingerprint: fingerprint };
|
|
1385
|
+
if (approved) {
|
|
1386
|
+
return {
|
|
1387
|
+
entrypointPaths,
|
|
1388
|
+
approvedBy: "BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA",
|
|
1389
|
+
...withFingerprint
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
if (fingerprint !== void 0 && fingerprint === input.approvedFingerprint) {
|
|
1393
|
+
return { entrypointPaths, approvedBy: "entrypoint-fingerprint", ...withFingerprint };
|
|
1394
|
+
}
|
|
1395
|
+
if (unmodeled.length > 0) {
|
|
1396
|
+
return {
|
|
1397
|
+
entrypointPaths,
|
|
1398
|
+
blockedReason: `trusted verification launcher is not modeled: ${unmodeled[0]}`
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const named = redefined.slice(0, MAX_REPORTED_PATHS).join(", ");
|
|
1402
|
+
const extra = redefined.length - MAX_REPORTED_PATHS;
|
|
1403
|
+
const more = extra > 0 ? ` (+${extra} more)` : "";
|
|
1404
|
+
return {
|
|
1405
|
+
entrypointPaths,
|
|
1406
|
+
blockedReason: `candidate changes redefine ${named}${more}`,
|
|
1407
|
+
...withFingerprint
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
if (unnormalized.length > 0) {
|
|
1411
|
+
return {
|
|
1412
|
+
entrypointPaths,
|
|
1413
|
+
blockedReason: `candidate change path cannot be compared: ${unnormalized[0]}`
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
return { entrypointPaths };
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/core/ci/local-ci-plan.ts
|
|
1420
|
+
function stepId(phase, index) {
|
|
1421
|
+
return `${phase}-${index + 1}`;
|
|
1422
|
+
}
|
|
1423
|
+
function buildLocalCiVerificationPlan(input) {
|
|
1424
|
+
const setupCommands = (input.setupCommands ?? []).map((command) => command.trim()).filter(Boolean);
|
|
1425
|
+
const verifyCommands = input.verifyCommands.map((command) => command.trim()).filter(Boolean);
|
|
1426
|
+
const steps = [];
|
|
1427
|
+
for (let index = 0; index < setupCommands.length; index += 1) {
|
|
1428
|
+
steps.push({
|
|
1429
|
+
id: stepId("setup", index),
|
|
1430
|
+
phase: "setup",
|
|
1431
|
+
command: setupCommands[index],
|
|
1432
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
for (let index = 0; index < verifyCommands.length; index += 1) {
|
|
1436
|
+
steps.push({
|
|
1437
|
+
id: stepId("verify", index),
|
|
1438
|
+
phase: "verify",
|
|
1439
|
+
command: verifyCommands[index],
|
|
1440
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
const allCommands = steps.map((step) => step.command);
|
|
1444
|
+
const authority = input.authority ?? resolveProjectVerificationAuthority(allCommands, {
|
|
1445
|
+
projectRoot: input.projectRoot,
|
|
1446
|
+
additionalAuthorityPaths: input.additionalAuthorityPaths,
|
|
1447
|
+
authorityPathsComplete: input.authorityPathsComplete
|
|
1448
|
+
});
|
|
1449
|
+
return { steps, authority };
|
|
1450
|
+
}
|
|
1451
|
+
function verificationPlanCommands(plan) {
|
|
1452
|
+
return plan.steps.map((step) => step.command);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/core/ci/local-ci-trust.ts
|
|
1456
|
+
function resolveApprovedEntrypointSha(trust) {
|
|
1457
|
+
return trust ? trust.approvedEntrypointSha : readEnv(BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA).value;
|
|
1458
|
+
}
|
|
1459
|
+
async function resolveHaltOverride(trust) {
|
|
1460
|
+
if (trust) return await trust.claimHaltOverride?.() === true;
|
|
1461
|
+
return readEnv(BUILD_LOCAL_CI_OVERRIDE_HALT).value === true;
|
|
1462
|
+
}
|
|
1463
|
+
function withApprovedEntrypointEnv(env) {
|
|
1464
|
+
const approved = readEnv(BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA).value;
|
|
1465
|
+
if (/^[a-f0-9]{40}$/i.test(approved ?? "")) env.BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA = approved;
|
|
1466
|
+
return env;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/core/ci/local-ci-authority.ts
|
|
1470
|
+
async function ensureTrustedCommit(repoRoot, remote, baseRef, baseSha) {
|
|
1471
|
+
let present = await execGit(["cat-file", "-e", `${baseSha}^{commit}`], { cwd: repoRoot });
|
|
1472
|
+
if (present.exitCode === 0) return;
|
|
1473
|
+
const fetched = await execGit(["fetch", "--quiet", remote, baseRef], { cwd: repoRoot });
|
|
1474
|
+
if (fetched.exitCode !== 0) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`Unable to fetch trusted base '${baseRef}': ${fetched.stderr || fetched.stdout}`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
present = await execGit(["cat-file", "-e", `${baseSha}^{commit}`], { cwd: repoRoot });
|
|
1480
|
+
if (present.exitCode !== 0) {
|
|
1481
|
+
throw new Error(`Trusted base commit ${baseSha} is unavailable after fetch.`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function admitTrustedBaseRef(baseRef, defaultBranch) {
|
|
1485
|
+
if (baseRef === defaultBranch) return;
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`Pull request base '${baseRef}' is not the repository default branch '${defaultBranch}'; refusing to publish a SHA-scoped attestation for a different base policy.`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
var InvalidGitDiffPathEncodingError = class extends Error {
|
|
1491
|
+
invalidPathBytes;
|
|
1492
|
+
constructor(invalidPathBytes) {
|
|
1493
|
+
super("Changed path is not valid UTF-8; exact Git path identity required.");
|
|
1494
|
+
this.name = "InvalidGitDiffPathEncodingError";
|
|
1495
|
+
this.invalidPathBytes = invalidPathBytes;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
function parseNulDelimitedDiffPathsStrictUtf8(stdout) {
|
|
1499
|
+
if (stdout.length === 0) return [];
|
|
1500
|
+
const parts = [];
|
|
1501
|
+
let start = 0;
|
|
1502
|
+
for (let i = 0; i < stdout.length; i++) {
|
|
1503
|
+
if (stdout[i] === 0) {
|
|
1504
|
+
parts.push(stdout.subarray(start, i));
|
|
1505
|
+
start = i + 1;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (start < stdout.length) {
|
|
1509
|
+
parts.push(stdout.subarray(start));
|
|
1510
|
+
}
|
|
1511
|
+
if (parts.length > 0 && parts[parts.length - 1].length === 0) {
|
|
1512
|
+
parts.pop();
|
|
1513
|
+
}
|
|
1514
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
1515
|
+
const paths = [];
|
|
1516
|
+
for (const part of parts) {
|
|
1517
|
+
if (part.length === 0) continue;
|
|
1518
|
+
try {
|
|
1519
|
+
paths.push(decoder.decode(part));
|
|
1520
|
+
} catch {
|
|
1521
|
+
throw new InvalidGitDiffPathEncodingError(Buffer.from(part));
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return paths;
|
|
1525
|
+
}
|
|
1526
|
+
async function diffFiles(repoRoot, baseSha, headSha) {
|
|
1527
|
+
const result = await execGit(
|
|
1528
|
+
["diff", "--name-only", "--no-renames", "-z", `${baseSha}...${headSha}`],
|
|
1529
|
+
{
|
|
1530
|
+
cwd: repoRoot,
|
|
1531
|
+
encoding: "buffer"
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
if (result.exitCode !== 0) {
|
|
1535
|
+
const detail = result.stderr.toString("utf8") || result.stdout.toString("utf8");
|
|
1536
|
+
throw new Error(`Unable to compare trusted base and candidate: ${detail}`);
|
|
1537
|
+
}
|
|
1538
|
+
return parseNulDelimitedDiffPathsStrictUtf8(result.stdout);
|
|
1539
|
+
}
|
|
1540
|
+
async function diffChangeSet(repoRoot, baseSha, headSha) {
|
|
1541
|
+
const result = await execGit(
|
|
1542
|
+
["diff", "--raw", "--no-abbrev", "--no-renames", "-z", `${baseSha}...${headSha}`],
|
|
1543
|
+
{
|
|
1544
|
+
cwd: repoRoot,
|
|
1545
|
+
encoding: "buffer"
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1548
|
+
if (result.exitCode !== 0) {
|
|
1549
|
+
const detail = result.stderr.toString("utf8") || result.stdout.toString("utf8");
|
|
1550
|
+
return {
|
|
1551
|
+
complete: false,
|
|
1552
|
+
reason: `Unable to read raw change-set metadata: ${detail}`,
|
|
1553
|
+
entries: []
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
return parseGitRawDiffChangeSet(result.stdout);
|
|
1557
|
+
}
|
|
1558
|
+
function emptyAuthority(reasons = []) {
|
|
1559
|
+
return { paths: [], complete: false, reasons };
|
|
1560
|
+
}
|
|
1561
|
+
function emptyChangeSet(reason) {
|
|
1562
|
+
return { complete: false, reason, entries: [] };
|
|
1563
|
+
}
|
|
1564
|
+
function fallbackDecision(candidateWorktree, reason, changedFiles = []) {
|
|
1565
|
+
const commands = resolveLocalCiFallbackCommands(candidateWorktree, changedFiles);
|
|
1566
|
+
const verificationAuthority = emptyAuthority([`Trusted-base policy unavailable: ${reason}`]);
|
|
1567
|
+
const verificationPlan = buildLocalCiVerificationPlan({
|
|
1568
|
+
setupCommands: [],
|
|
1569
|
+
verifyCommands: commands,
|
|
1570
|
+
authority: verificationAuthority,
|
|
1571
|
+
projectRoot: candidateWorktree
|
|
1572
|
+
});
|
|
1573
|
+
return {
|
|
1574
|
+
enabled: true,
|
|
1575
|
+
commands: verificationPlanCommands(verificationPlan),
|
|
1576
|
+
setupCommands: [],
|
|
1577
|
+
setupUnavailable: [],
|
|
1578
|
+
verifyCommands: commands,
|
|
1579
|
+
authorityPaths: [],
|
|
1580
|
+
authorityPathsComplete: false,
|
|
1581
|
+
validationProfiles: [],
|
|
1582
|
+
statusContext: DEFAULT_LOCAL_CI_STATUS_CONTEXT,
|
|
1583
|
+
maxConcurrent: DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
1584
|
+
trusted: false,
|
|
1585
|
+
changedFiles,
|
|
1586
|
+
verificationAuthority,
|
|
1587
|
+
verificationPlan,
|
|
1588
|
+
// Untrusted decisions never publish success; keep the block explicit anyway so
|
|
1589
|
+
// no publication path can read an absent check as a cleared one.
|
|
1590
|
+
commandEntrypoint: {
|
|
1591
|
+
entrypointPaths: [],
|
|
1592
|
+
blockedReason: `trusted-base policy unavailable: ${reason}`
|
|
1593
|
+
},
|
|
1594
|
+
validationProfile: LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
1595
|
+
validationProfileReason: boundLocalCiValidationProfileReason(
|
|
1596
|
+
`Trusted-base policy unavailable: ${reason}`
|
|
1597
|
+
)
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function buildTrustedDecision(input) {
|
|
1601
|
+
const trustedConfig = resolveLocalCiConfig(input.baseWorktree, input.changedFiles);
|
|
1602
|
+
const verificationAuthority = resolveProjectVerificationAuthority(trustedConfig.commands, {
|
|
1603
|
+
projectRoot: input.baseWorktree,
|
|
1604
|
+
additionalAuthorityPaths: trustedConfig.authorityPaths,
|
|
1605
|
+
authorityPathsComplete: trustedConfig.authorityPathsComplete
|
|
1606
|
+
});
|
|
1607
|
+
const verificationPlan = buildLocalCiVerificationPlan({
|
|
1608
|
+
setupCommands: trustedConfig.setupCommands,
|
|
1609
|
+
verifyCommands: trustedConfig.verifyCommands,
|
|
1610
|
+
authority: verificationAuthority,
|
|
1611
|
+
projectRoot: input.baseWorktree,
|
|
1612
|
+
additionalAuthorityPaths: trustedConfig.authorityPaths,
|
|
1613
|
+
authorityPathsComplete: trustedConfig.authorityPathsComplete
|
|
1614
|
+
});
|
|
1615
|
+
const commandEntrypoint = checkTrustedCommandEntrypoints({
|
|
1616
|
+
commands: verificationPlanCommands(verificationPlan),
|
|
1617
|
+
changedFiles: input.changedFiles,
|
|
1618
|
+
changedFilesComplete: input.changedFilesComplete,
|
|
1619
|
+
trustedRoot: input.baseWorktree,
|
|
1620
|
+
candidateRoot: input.candidateWorktree,
|
|
1621
|
+
candidateSha: input.candidateSha,
|
|
1622
|
+
approvedCandidateSha: input.approvedEntrypointSha,
|
|
1623
|
+
approvedFingerprint: input.approvedEntrypointFingerprint
|
|
1624
|
+
});
|
|
1625
|
+
const profileSelection = selectLocalCiValidationProfile({
|
|
1626
|
+
trusted: true,
|
|
1627
|
+
verificationAuthority,
|
|
1628
|
+
changedFiles: input.changedFiles,
|
|
1629
|
+
changeSet: input.changeSet,
|
|
1630
|
+
profiles: trustedConfig.validationProfiles
|
|
1631
|
+
});
|
|
1632
|
+
return {
|
|
1633
|
+
...trustedConfig,
|
|
1634
|
+
// Prefer plan ordering while remaining backward compatible with flat commands.
|
|
1635
|
+
commands: verificationPlanCommands(verificationPlan),
|
|
1636
|
+
trusted: true,
|
|
1637
|
+
trustedBaseRef: input.trustedBaseRef,
|
|
1638
|
+
trustedBaseSha: input.trustedBaseSha,
|
|
1639
|
+
pullRequest: input.pullRequest,
|
|
1640
|
+
changedFiles: input.changedFiles,
|
|
1641
|
+
verificationAuthority,
|
|
1642
|
+
verificationPlan,
|
|
1643
|
+
commandEntrypoint,
|
|
1644
|
+
validationProfile: profileSelection.validationProfile,
|
|
1645
|
+
validationProfileReason: profileSelection.reason
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
async function resolveLocalCiAuthority(record, candidateWorktree, repoRoot, dependencyOverrides = {}, trust) {
|
|
1649
|
+
const approvedEntrypointSha = resolveApprovedEntrypointSha(trust);
|
|
1650
|
+
const approvedEntrypointFingerprint = trust?.approvedEntrypointFingerprint;
|
|
1651
|
+
const deps = {
|
|
1652
|
+
findPullRequest: findGitHubPullRequest,
|
|
1653
|
+
waitForPrSync: () => delay3(2e3),
|
|
1654
|
+
resolveDefaultBranch: resolveGitHubDefaultBranch,
|
|
1655
|
+
resolveBranchSha: resolveGitHubBranchSha,
|
|
1656
|
+
createWorktree: createLocalCiWorktree,
|
|
1657
|
+
removeWorktree: removeLocalCiWorktree,
|
|
1658
|
+
ensureTrustedCommit,
|
|
1659
|
+
diffFiles,
|
|
1660
|
+
diffChangeSet,
|
|
1661
|
+
...dependencyOverrides
|
|
1662
|
+
};
|
|
1663
|
+
let baseWorktree;
|
|
1664
|
+
let changedFilesForFallback = [];
|
|
1665
|
+
try {
|
|
1666
|
+
const defaultBranch = await deps.resolveDefaultBranch(record.repository);
|
|
1667
|
+
let pullRequest = await deps.findPullRequest({
|
|
1668
|
+
repository: record.repository,
|
|
1669
|
+
branch: record.branch,
|
|
1670
|
+
base: defaultBranch,
|
|
1671
|
+
attempts: 1
|
|
1672
|
+
});
|
|
1673
|
+
for (let attempt = 1; pullRequest && pullRequest.headSha.toLowerCase() !== record.sha.toLowerCase(); attempt++) {
|
|
1674
|
+
if (attempt === 6) {
|
|
1675
|
+
throw new Error(
|
|
1676
|
+
`PR head synchronization pending: expected ${record.sha.slice(0, 12)}, observed ${pullRequest.headSha.slice(0, 12)} after 6 reads; finish pushing, then run build ci reconcile.`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
await deps.waitForPrSync();
|
|
1680
|
+
const refreshed = await deps.findPullRequest({
|
|
1681
|
+
repository: record.repository,
|
|
1682
|
+
branch: record.branch,
|
|
1683
|
+
number: pullRequest.number,
|
|
1684
|
+
base: defaultBranch,
|
|
1685
|
+
attempts: 1
|
|
1686
|
+
});
|
|
1687
|
+
if (refreshed) pullRequest = refreshed;
|
|
1688
|
+
}
|
|
1689
|
+
const trustedBaseRef = pullRequest?.baseRefName ?? defaultBranch;
|
|
1690
|
+
await admitTrustedBaseRef(trustedBaseRef, defaultBranch);
|
|
1691
|
+
const trustedBaseSha = pullRequest?.baseSha ?? await deps.resolveBranchSha({ repository: record.repository, branch: trustedBaseRef });
|
|
1692
|
+
await deps.ensureTrustedCommit(repoRoot, record.remote, trustedBaseRef, trustedBaseSha);
|
|
1693
|
+
const pullRequestVisibility = pullRequest ? {
|
|
1694
|
+
number: pullRequest.number,
|
|
1695
|
+
headSha: pullRequest.headSha,
|
|
1696
|
+
baseRefName: pullRequest.baseRefName,
|
|
1697
|
+
baseSha: pullRequest.baseSha,
|
|
1698
|
+
draft: pullRequest.draft
|
|
1699
|
+
} : void 0;
|
|
1700
|
+
let changedFiles;
|
|
1701
|
+
try {
|
|
1702
|
+
changedFiles = await deps.diffFiles(repoRoot, trustedBaseSha, record.sha);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
if (error instanceof InvalidGitDiffPathEncodingError) {
|
|
1705
|
+
baseWorktree = await deps.createWorktree(repoRoot, trustedBaseSha);
|
|
1706
|
+
return buildTrustedDecision({
|
|
1707
|
+
baseWorktree,
|
|
1708
|
+
candidateWorktree,
|
|
1709
|
+
candidateSha: record.sha,
|
|
1710
|
+
changedFiles: [],
|
|
1711
|
+
// Undecodable path bytes drop the whole name-only set, so no trusted
|
|
1712
|
+
// command entrypoint can be cleared from it.
|
|
1713
|
+
changedFilesComplete: false,
|
|
1714
|
+
changeSet: emptyChangeSet(
|
|
1715
|
+
"Changed path is not valid UTF-8; exact Git path identity required."
|
|
1716
|
+
),
|
|
1717
|
+
trustedBaseRef,
|
|
1718
|
+
trustedBaseSha,
|
|
1719
|
+
pullRequest: pullRequestVisibility,
|
|
1720
|
+
approvedEntrypointSha,
|
|
1721
|
+
approvedEntrypointFingerprint
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
throw error;
|
|
1725
|
+
}
|
|
1726
|
+
changedFilesForFallback = changedFiles;
|
|
1727
|
+
let changeSet;
|
|
1728
|
+
try {
|
|
1729
|
+
changeSet = await deps.diffChangeSet(repoRoot, trustedBaseSha, record.sha);
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1732
|
+
changeSet = emptyChangeSet(`Raw change-set metadata unavailable: ${detail}`);
|
|
1733
|
+
}
|
|
1734
|
+
baseWorktree = await deps.createWorktree(repoRoot, trustedBaseSha);
|
|
1735
|
+
return buildTrustedDecision({
|
|
1736
|
+
baseWorktree,
|
|
1737
|
+
candidateWorktree,
|
|
1738
|
+
candidateSha: record.sha,
|
|
1739
|
+
changedFiles,
|
|
1740
|
+
changedFilesComplete: true,
|
|
1741
|
+
changeSet,
|
|
1742
|
+
trustedBaseRef,
|
|
1743
|
+
trustedBaseSha,
|
|
1744
|
+
pullRequest: pullRequestVisibility,
|
|
1745
|
+
approvedEntrypointSha,
|
|
1746
|
+
approvedEntrypointFingerprint
|
|
1747
|
+
});
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1750
|
+
return fallbackDecision(candidateWorktree, reason, changedFilesForFallback);
|
|
1751
|
+
} finally {
|
|
1752
|
+
if (baseWorktree) {
|
|
1753
|
+
await deps.removeWorktree(repoRoot, baseWorktree).catch(() => void 0);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function resolveEntrypointFingerprint(input) {
|
|
1758
|
+
const worktrees = [];
|
|
1759
|
+
try {
|
|
1760
|
+
await ensureTrustedCommit(input.repoRoot, input.remote, input.baseRef, input.baseSha);
|
|
1761
|
+
const changedFiles = await diffFiles(input.repoRoot, input.baseSha, input.headSha);
|
|
1762
|
+
const baseWorktree = await createLocalCiWorktree(input.repoRoot, input.baseSha);
|
|
1763
|
+
worktrees.push(baseWorktree);
|
|
1764
|
+
const candidateWorktree = await createLocalCiWorktree(input.repoRoot, input.headSha);
|
|
1765
|
+
worktrees.push(candidateWorktree);
|
|
1766
|
+
return buildTrustedDecision({
|
|
1767
|
+
baseWorktree,
|
|
1768
|
+
candidateWorktree,
|
|
1769
|
+
candidateSha: input.headSha,
|
|
1770
|
+
changedFiles,
|
|
1771
|
+
changedFilesComplete: true,
|
|
1772
|
+
changeSet: emptyChangeSet("Raw change-set metadata is not needed for a fingerprint."),
|
|
1773
|
+
trustedBaseRef: input.baseRef,
|
|
1774
|
+
trustedBaseSha: input.baseSha
|
|
1775
|
+
}).commandEntrypoint.entrypointFingerprint;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return void 0;
|
|
1778
|
+
} finally {
|
|
1779
|
+
for (const worktree of worktrees) {
|
|
1780
|
+
await removeLocalCiWorktree(input.repoRoot, worktree).catch(() => void 0);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/core/ci/merge-queue-approval-hold.ts
|
|
1786
|
+
var EntrypointApprovalRequired = class extends Error {
|
|
1787
|
+
constructor(record) {
|
|
1788
|
+
super("Local CI verification entrypoint requires exact-head approval.");
|
|
1789
|
+
this.record = record;
|
|
1790
|
+
}
|
|
1791
|
+
record;
|
|
1792
|
+
};
|
|
1793
|
+
var FALLBACK_REASON = "trusted verification entrypoint changed";
|
|
1794
|
+
function blockedReason(record) {
|
|
1795
|
+
return /entrypoint: (.+?)\. Review the change/s.exec(record.error ?? "")?.[1] ?? FALLBACK_REASON;
|
|
1796
|
+
}
|
|
1797
|
+
function approvalFor(request, headSha) {
|
|
1798
|
+
return request.trustGrants?.find(
|
|
1799
|
+
(grant) => grant.kind === "entrypoint-approval" && grant.sourceRequestId === request.id && grant.scope.headSha === headSha
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
function dispositionPatch(current, error, now) {
|
|
1803
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1804
|
+
if (current.status !== "queued" && current.status !== "running") return { lastError: detail };
|
|
1805
|
+
if (error instanceof EntrypointApprovalRequired) {
|
|
1806
|
+
const approval = approvalFor(current, error.record.sha);
|
|
1807
|
+
const ranUnderApproval = approval && Date.parse(approval.grantedAt) <= Date.parse(error.record.updatedAt);
|
|
1808
|
+
if (!ranUnderApproval) {
|
|
1809
|
+
return {
|
|
1810
|
+
status: "awaiting_approval",
|
|
1811
|
+
entrypointHold: {
|
|
1812
|
+
headSha: error.record.sha,
|
|
1813
|
+
reason: blockedReason(error.record),
|
|
1814
|
+
...error.record.entrypointFingerprint ? { fingerprint: error.record.entrypointFingerprint } : {},
|
|
1815
|
+
heldAt: now.toISOString()
|
|
1816
|
+
},
|
|
1817
|
+
lastError: void 0
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return { status: "needs_attention", lastError: detail };
|
|
1822
|
+
}
|
|
1823
|
+
function releaseHold(request, now) {
|
|
1824
|
+
if (request.status !== "awaiting_approval" || !request.entrypointHold) return {};
|
|
1825
|
+
return {
|
|
1826
|
+
status: "queued",
|
|
1827
|
+
workerToken: void 0,
|
|
1828
|
+
lastError: void 0,
|
|
1829
|
+
entrypointHold: { ...request.entrypointHold, releasedAt: now.toISOString() }
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function releaseIfApproved(request, now) {
|
|
1833
|
+
return approvalFor(request, request.entrypointHold?.headSha) ? releaseHold(request, now) : {};
|
|
1834
|
+
}
|
|
1835
|
+
function approvalCommand(request) {
|
|
1836
|
+
return `build ci merge ${request.prNumber} --sha ${request.reviewedSha} --approve-entrypoint ${request.entrypointHold?.headSha ?? request.acceptedHeadSha}`;
|
|
1837
|
+
}
|
|
1838
|
+
async function resumeApprovedHolds(store, repository, deps, now = /* @__PURE__ */ new Date()) {
|
|
1839
|
+
const released = [];
|
|
1840
|
+
for (const request of await store.list(repository)) {
|
|
1841
|
+
const hold = request.entrypointHold;
|
|
1842
|
+
if (request.status !== "awaiting_approval" || !hold) continue;
|
|
1843
|
+
const pr = await deps.readPullRequest(request).catch(() => void 0);
|
|
1844
|
+
if (pr?.state !== "open" || pr.headSha !== request.acceptedHeadSha) continue;
|
|
1845
|
+
if (!approvalFor(request, hold.headSha)) {
|
|
1846
|
+
const run = await deps.inspectValidation(request).catch(() => void 0);
|
|
1847
|
+
const green = run?.status === "succeeded" && !run.entrypointApprovalPending && run.sha === hold.headSha && request.acceptedHeadSha === hold.headSha;
|
|
1848
|
+
if (!green) continue;
|
|
1849
|
+
}
|
|
1850
|
+
const result = await store.releaseApprovalHold(repository, request.id, request.version, now);
|
|
1851
|
+
if (result) released.push(result);
|
|
1852
|
+
}
|
|
1853
|
+
return released;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/core/ci/landing-log.ts
|
|
1857
|
+
import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat2 } from "fs/promises";
|
|
1858
|
+
import { hostname } from "os";
|
|
1859
|
+
import path3 from "path";
|
|
1860
|
+
|
|
1861
|
+
// src/core/ci/local-ci-gate-evidence.ts
|
|
1862
|
+
var EVIDENCE_MARKER = "[release:gate] result-json=";
|
|
1863
|
+
var MAX_SCAN_CHARS = 2e5;
|
|
1864
|
+
var MAX_LABEL_LENGTH = 120;
|
|
1865
|
+
var MAX_DESCRIPTION_LENGTH = 140;
|
|
1866
|
+
function stringArray(value) {
|
|
1867
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
1868
|
+
return void 0;
|
|
1869
|
+
}
|
|
1870
|
+
return [...value];
|
|
1871
|
+
}
|
|
1872
|
+
function parseGateEvidence(output) {
|
|
1873
|
+
const evidence = parseGateResultJson(output);
|
|
1874
|
+
if (evidence === void 0) return void 0;
|
|
1875
|
+
return readGateEvidence(evidence);
|
|
1876
|
+
}
|
|
1877
|
+
function parseGateResultJson(output) {
|
|
1878
|
+
if (output.length === 0) return void 0;
|
|
1879
|
+
const scanned = output.length > MAX_SCAN_CHARS ? output.slice(-MAX_SCAN_CHARS) : output;
|
|
1880
|
+
const markerIndex = scanned.lastIndexOf(EVIDENCE_MARKER);
|
|
1881
|
+
if (markerIndex === -1) return void 0;
|
|
1882
|
+
const jsonStart = markerIndex + EVIDENCE_MARKER.length;
|
|
1883
|
+
const lineEnd = scanned.indexOf("\n", jsonStart);
|
|
1884
|
+
const rawJson = (lineEnd === -1 ? scanned.slice(jsonStart) : scanned.slice(jsonStart, lineEnd)).trim();
|
|
1885
|
+
if (rawJson.length === 0) return void 0;
|
|
1886
|
+
let parsed;
|
|
1887
|
+
try {
|
|
1888
|
+
parsed = JSON.parse(rawJson);
|
|
1889
|
+
} catch {
|
|
1890
|
+
return void 0;
|
|
1891
|
+
}
|
|
1892
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
1893
|
+
return parsed;
|
|
1894
|
+
}
|
|
1895
|
+
function readGateEvidence(evidence) {
|
|
1896
|
+
const result = {};
|
|
1897
|
+
if (typeof evidence.outcome === "string" && evidence.outcome.length > 0) {
|
|
1898
|
+
result.outcome = evidence.outcome.slice(0, MAX_LABEL_LENGTH);
|
|
1899
|
+
}
|
|
1900
|
+
const canonical = evidence.canonicalFailure;
|
|
1901
|
+
if (typeof canonical === "object" && canonical !== null) {
|
|
1902
|
+
const record = canonical;
|
|
1903
|
+
if (typeof record.label === "string" && record.label.length > 0) {
|
|
1904
|
+
result.canonicalFailureLabel = record.label.slice(0, MAX_LABEL_LENGTH);
|
|
1905
|
+
}
|
|
1906
|
+
if (typeof record.reason === "string" && record.reason.length > 0) {
|
|
1907
|
+
result.canonicalFailureReason = record.reason.slice(0, MAX_LABEL_LENGTH);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const planned = evidence.planned;
|
|
1911
|
+
if (typeof planned === "object" && planned !== null) {
|
|
1912
|
+
const plannedRecord = planned;
|
|
1913
|
+
const count = plannedRecord.count;
|
|
1914
|
+
if (typeof count === "number" && Number.isFinite(count)) result.plannedCount = count;
|
|
1915
|
+
const labels = stringArray(plannedRecord.labels);
|
|
1916
|
+
if (labels !== void 0) result.plannedLabels = labels;
|
|
1917
|
+
}
|
|
1918
|
+
const executed = evidence.executed;
|
|
1919
|
+
if (typeof executed === "object" && executed !== null) {
|
|
1920
|
+
const count = executed.count;
|
|
1921
|
+
if (typeof count === "number" && Number.isFinite(count)) result.executedCount = count;
|
|
1922
|
+
}
|
|
1923
|
+
const manifest = evidence.manifest;
|
|
1924
|
+
if (typeof manifest === "object" && manifest !== null) {
|
|
1925
|
+
const digest = manifest.digest;
|
|
1926
|
+
if (typeof digest === "string" && /^[a-f0-9]{64}$/.test(digest)) {
|
|
1927
|
+
result.manifestDigest = digest;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const steps = evidence.steps;
|
|
1931
|
+
if (Array.isArray(steps)) {
|
|
1932
|
+
const passedLabels = [];
|
|
1933
|
+
const failedLabels = [];
|
|
1934
|
+
let valid = true;
|
|
1935
|
+
for (const step of steps) {
|
|
1936
|
+
if (typeof step !== "object" || step === null) {
|
|
1937
|
+
valid = false;
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
const stepRecord = step;
|
|
1941
|
+
if (typeof stepRecord.label !== "string" || stepRecord.label.length === 0 || stepRecord.outcome !== "ok" && stepRecord.outcome !== "fail") {
|
|
1942
|
+
valid = false;
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
(stepRecord.outcome === "ok" ? passedLabels : failedLabels).push(stepRecord.label);
|
|
1946
|
+
}
|
|
1947
|
+
if (valid) {
|
|
1948
|
+
result.passedLabels = passedLabels;
|
|
1949
|
+
result.failedLabels = failedLabels;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const notRun = evidence.notRun;
|
|
1953
|
+
if (typeof notRun === "object" && notRun !== null) {
|
|
1954
|
+
const labels = stringArray(notRun.labels);
|
|
1955
|
+
if (labels !== void 0) result.notRunLabels = labels;
|
|
1956
|
+
}
|
|
1957
|
+
const retry = evidence.retry;
|
|
1958
|
+
if (typeof retry === "object" && retry !== null) {
|
|
1959
|
+
const retryRecord = retry;
|
|
1960
|
+
if (typeof retryRecord.applied === "boolean") result.retryApplied = retryRecord.applied;
|
|
1961
|
+
if (typeof retryRecord.sourceManifestDigest === "string" && /^[a-f0-9]{64}$/.test(retryRecord.sourceManifestDigest)) {
|
|
1962
|
+
result.retrySourceManifestDigest = retryRecord.sourceManifestDigest;
|
|
1963
|
+
}
|
|
1964
|
+
const reusedLabels = stringArray(retryRecord.reusedLabels);
|
|
1965
|
+
if (reusedLabels !== void 0) result.retryReusedLabels = reusedLabels;
|
|
1966
|
+
const rerunLabels = stringArray(retryRecord.rerunLabels);
|
|
1967
|
+
if (rerunLabels !== void 0) result.retryRerunLabels = rerunLabels;
|
|
1968
|
+
}
|
|
1969
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1970
|
+
}
|
|
1971
|
+
function describeGateFailure(evidence) {
|
|
1972
|
+
if (evidence?.canonicalFailureLabel === void 0) return void 0;
|
|
1973
|
+
const reason = evidence.canonicalFailureReason ? ` (${evidence.canonicalFailureReason})` : "";
|
|
1974
|
+
const message2 = `Local validation failed: ${evidence.canonicalFailureLabel}${reason}`;
|
|
1975
|
+
return message2.length <= MAX_DESCRIPTION_LENGTH ? message2 : `${message2.slice(0, MAX_DESCRIPTION_LENGTH - 1)}\u2026`;
|
|
1976
|
+
}
|
|
1977
|
+
function describeUnavailableDependencySetup(unavailable) {
|
|
1978
|
+
if (!unavailable || unavailable.length === 0) return "";
|
|
1979
|
+
const entries = unavailable.map((entry) => `${entry.language} (${entry.manifest}): ${entry.reason}`).join("; ");
|
|
1980
|
+
return ` Dependency setup unavailable: ${entries}.`;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/core/ci/landing-log.ts
|
|
1984
|
+
var LANDING_LOG_FILE = "landing-log.jsonl";
|
|
1985
|
+
var MAX_LANDING_LOG_BYTES = 8 * 1024 * 1024;
|
|
1986
|
+
var MAX_ERROR_LENGTH = 300;
|
|
1987
|
+
function landingLogPath(stateRoot) {
|
|
1988
|
+
return path3.join(stateRoot, LANDING_LOG_FILE);
|
|
1989
|
+
}
|
|
1990
|
+
function bounded(value) {
|
|
1991
|
+
if (value === void 0) return void 0;
|
|
1992
|
+
return value.length > MAX_ERROR_LENGTH ? `${value.slice(0, MAX_ERROR_LENGTH)}...` : value;
|
|
1993
|
+
}
|
|
1994
|
+
function compact(entry) {
|
|
1995
|
+
return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== void 0));
|
|
1996
|
+
}
|
|
1997
|
+
async function isFull(file, maxBytes) {
|
|
1998
|
+
try {
|
|
1999
|
+
return (await stat2(file)).size >= maxBytes;
|
|
2000
|
+
} catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function rotateIfFull(file, maxBytes) {
|
|
2005
|
+
if (!await isFull(file, maxBytes)) return;
|
|
2006
|
+
const release = await acquireFencedFileLock(`${file}.rotate`, {
|
|
2007
|
+
stale: 1e4,
|
|
2008
|
+
retries: { retries: 20, minTimeout: 25, maxTimeout: 100 },
|
|
2009
|
+
description: "Landing log rotation"
|
|
2010
|
+
});
|
|
2011
|
+
try {
|
|
2012
|
+
if (await isFull(file, maxBytes)) await rename2(file, `${file}.1`);
|
|
2013
|
+
} finally {
|
|
2014
|
+
await release();
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
async function appendLandingLogEntry(stateRoot, kind, fields, options = {}) {
|
|
2018
|
+
try {
|
|
2019
|
+
const file = landingLogPath(stateRoot);
|
|
2020
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2021
|
+
await mkdir2(stateRoot, { recursive: true, mode: 448 });
|
|
2022
|
+
await rotateIfFull(file, options.maxBytes ?? MAX_LANDING_LOG_BYTES);
|
|
2023
|
+
const line = JSON.stringify(
|
|
2024
|
+
compact({ v: 1, ts: now.toISOString(), kind, host: hostname(), stateRoot, ...fields })
|
|
2025
|
+
);
|
|
2026
|
+
await appendFile(file, `${line}
|
|
2027
|
+
`, { encoding: "utf8", mode: 384 });
|
|
2028
|
+
} catch {
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function gateLandingFields(record) {
|
|
2032
|
+
const commands = record.commands ?? [];
|
|
2033
|
+
let gate;
|
|
2034
|
+
for (const command of [...commands].reverse()) {
|
|
2035
|
+
const parsed = command.output ? parseGateResultJson(command.output) : void 0;
|
|
2036
|
+
if (parsed) {
|
|
2037
|
+
const steps = Array.isArray(parsed.steps) ? parsed.steps.flatMap((step) => {
|
|
2038
|
+
if (typeof step !== "object" || step === null) return [];
|
|
2039
|
+
const { label, outcome, durationMs } = step;
|
|
2040
|
+
return typeof label === "string" ? [{ label, outcome, durationMs }] : [];
|
|
2041
|
+
}) : void 0;
|
|
2042
|
+
gate = compact({
|
|
2043
|
+
outcome: parsed.outcome,
|
|
2044
|
+
wallMs: parsed.wallMs,
|
|
2045
|
+
concurrency: parsed.concurrency,
|
|
2046
|
+
changedTests: parsed.changedTests,
|
|
2047
|
+
steps
|
|
2048
|
+
});
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return compact({
|
|
2053
|
+
repository: record.repository,
|
|
2054
|
+
sha: record.sha,
|
|
2055
|
+
branch: record.branch,
|
|
2056
|
+
pr: record.pullRequestNumber,
|
|
2057
|
+
status: record.status,
|
|
2058
|
+
error: bounded(record.error),
|
|
2059
|
+
trustedBaseSha: record.trustedBaseSha,
|
|
2060
|
+
validationProfile: record.validationProfile,
|
|
2061
|
+
submittedAt: record.submittedAt ?? record.createdAt,
|
|
2062
|
+
startedAt: record.startedAt,
|
|
2063
|
+
queueWaitMs: record.queueWaitMs,
|
|
2064
|
+
retryOfSha: record.retryOfSha,
|
|
2065
|
+
gateRetryApplied: record.gateRetry?.applied,
|
|
2066
|
+
entrypointApprovalPending: record.entrypointApprovalPending,
|
|
2067
|
+
failingTestCount: record.failingTests?.length,
|
|
2068
|
+
commands: commands.map(
|
|
2069
|
+
(command) => compact({ id: command.id, exitCode: command.exitCode, durationMs: command.durationMs })
|
|
2070
|
+
),
|
|
2071
|
+
gate
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
function queueLandingFields(request) {
|
|
2075
|
+
const created = Date.parse(request.createdAt);
|
|
2076
|
+
const updated2 = Date.parse(request.updatedAt);
|
|
2077
|
+
return compact({
|
|
2078
|
+
repository: request.repository,
|
|
2079
|
+
requestId: request.id,
|
|
2080
|
+
pr: request.prNumber,
|
|
2081
|
+
reviewedSha: request.reviewedSha,
|
|
2082
|
+
acceptedHeadSha: request.acceptedHeadSha,
|
|
2083
|
+
status: request.status,
|
|
2084
|
+
error: bounded(request.lastError),
|
|
2085
|
+
preparationCount: request.preparationCount,
|
|
2086
|
+
recoveryCount: request.recoveryCount,
|
|
2087
|
+
mergeCommit: request.mergeCommit,
|
|
2088
|
+
batchMemberCount: request.batchMembers?.length,
|
|
2089
|
+
batchedIntoPr: request.batchedInto?.prNumber,
|
|
2090
|
+
batchAttempted: request.batchAttempted,
|
|
2091
|
+
createdAt: request.createdAt,
|
|
2092
|
+
durationMs: Number.isFinite(created) && Number.isFinite(updated2) ? updated2 - created : void 0
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/core/ci/local-ci-coordinator.ts
|
|
2097
|
+
import { spawn } from "child_process";
|
|
2098
|
+
import { closeSync, openSync } from "fs";
|
|
2099
|
+
import path4 from "path";
|
|
2100
|
+
import { setTimeout as delay4 } from "timers/promises";
|
|
2101
|
+
import { z as z3 } from "zod";
|
|
2102
|
+
|
|
2103
|
+
// src/core/ci/local-ci-base-binding-recheck.ts
|
|
2104
|
+
async function selectBaseBindingRechecks(records, repository, github) {
|
|
2105
|
+
const rechecks = /* @__PURE__ */ new Map();
|
|
2106
|
+
const candidates = records.filter(
|
|
2107
|
+
(record) => record.status === "succeeded" && record.trustedBaseRef !== void 0 && record.pullRequestNumber !== void 0 && !hasOutstandingRemoteStatusPublication(record)
|
|
2108
|
+
);
|
|
2109
|
+
if (candidates.length === 0) return rechecks;
|
|
2110
|
+
let open;
|
|
2111
|
+
try {
|
|
2112
|
+
open = await github.listOpenPullRequests(repository);
|
|
2113
|
+
} catch {
|
|
2114
|
+
return rechecks;
|
|
2115
|
+
}
|
|
2116
|
+
const mayBeTruncated = open.size >= OPEN_PULL_REQUEST_LIST_LIMIT;
|
|
2117
|
+
for (const record of candidates) {
|
|
2118
|
+
const number = record.pullRequestNumber;
|
|
2119
|
+
if (!number) continue;
|
|
2120
|
+
const pullRequest = open.get(number) ?? (mayBeTruncated ? await github.findPullRequest({
|
|
2121
|
+
repository: record.repository,
|
|
2122
|
+
branch: record.branch,
|
|
2123
|
+
number,
|
|
2124
|
+
attempts: 1
|
|
2125
|
+
}).catch(() => null) : null);
|
|
2126
|
+
if (pullRequest) rechecks.set(record.sha, pullRequest);
|
|
2127
|
+
}
|
|
2128
|
+
return rechecks;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// src/core/ci/local-ci-execution.ts
|
|
2132
|
+
import { appendFileSync, statSync } from "fs";
|
|
2133
|
+
|
|
2134
|
+
// src/core/ci/local-ci-executed-tree.ts
|
|
2135
|
+
var EXECUTED_TREE_GIT_TIMEOUT_MS = 15e3;
|
|
2136
|
+
async function resolveExecutedTree(input) {
|
|
2137
|
+
try {
|
|
2138
|
+
const result = await execGit(["rev-parse", "--verify", `${input.candidateSha}^{tree}`], {
|
|
2139
|
+
cwd: input.repoRoot,
|
|
2140
|
+
timeoutMs: EXECUTED_TREE_GIT_TIMEOUT_MS
|
|
2141
|
+
});
|
|
2142
|
+
if (result.exitCode !== 0) return void 0;
|
|
2143
|
+
const tree = result.stdout.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
2144
|
+
return tree !== void 0 && /^[0-9a-f]{40,64}$/.test(tree) ? tree : void 0;
|
|
2145
|
+
} catch {
|
|
2146
|
+
return void 0;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
async function worktreeMatchesExecutedTree(input) {
|
|
2150
|
+
const headTree = await resolveExecutedTree({
|
|
2151
|
+
repoRoot: input.worktreePath,
|
|
2152
|
+
candidateSha: "HEAD"
|
|
2153
|
+
});
|
|
2154
|
+
if (headTree === void 0 || headTree !== input.executedTree) {
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
await execGit(["update-index", "-q", "--refresh"], {
|
|
2159
|
+
cwd: input.worktreePath,
|
|
2160
|
+
timeoutMs: EXECUTED_TREE_GIT_TIMEOUT_MS
|
|
2161
|
+
});
|
|
2162
|
+
const diff = await execGit(["diff-index", "--quiet", "HEAD", "--"], {
|
|
2163
|
+
cwd: input.worktreePath,
|
|
2164
|
+
timeoutMs: EXECUTED_TREE_GIT_TIMEOUT_MS
|
|
2165
|
+
});
|
|
2166
|
+
return diff.exitCode === 0;
|
|
2167
|
+
} catch {
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// src/core/ci/local-ci-gate-retry.ts
|
|
2173
|
+
function sameOrderedValues(left, right) {
|
|
2174
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2175
|
+
}
|
|
2176
|
+
function unique(values) {
|
|
2177
|
+
return new Set(values).size === values.length;
|
|
2178
|
+
}
|
|
2179
|
+
function resolveGateRetryPlan(input) {
|
|
2180
|
+
const { current, source } = input;
|
|
2181
|
+
if (current.retryOfSha === void 0 || current.retryOfSha !== source.sha || current.sha !== source.sha || source.status !== "failed" || source.retryOfSha !== void 0 || source.repository !== current.repository || source.branch !== current.branch || source.trustedBaseSha?.toLowerCase() !== input.trustedBaseSha.toLowerCase() || (source.validationProfile ?? "full") !== input.validationProfile) {
|
|
2182
|
+
return void 0;
|
|
2183
|
+
}
|
|
2184
|
+
const failedCommandIndex = source.commands.findIndex((command) => command.exitCode !== 0);
|
|
2185
|
+
if (failedCommandIndex < 0 || source.commands.some(
|
|
2186
|
+
(command, index) => index > failedCommandIndex || index < failedCommandIndex && command.exitCode !== 0
|
|
2187
|
+
)) {
|
|
2188
|
+
return void 0;
|
|
2189
|
+
}
|
|
2190
|
+
const currentStep = input.steps[failedCommandIndex];
|
|
2191
|
+
const failedCommand = source.commands[failedCommandIndex];
|
|
2192
|
+
if (currentStep === void 0 || failedCommand === void 0 || currentStep.command !== failedCommand.command || source.commands.slice(0, failedCommandIndex).some((command, index) => command.command !== input.steps[index]?.command)) {
|
|
2193
|
+
return void 0;
|
|
2194
|
+
}
|
|
2195
|
+
const evidence = parseGateEvidence(failedCommand.output);
|
|
2196
|
+
const planned = evidence?.plannedLabels;
|
|
2197
|
+
const passed = evidence?.passedLabels;
|
|
2198
|
+
const failed = evidence?.failedLabels;
|
|
2199
|
+
const notRun = evidence?.notRunLabels;
|
|
2200
|
+
if (evidence?.outcome !== "fail" || evidence.manifestDigest === void 0 || planned === void 0 || passed === void 0 || failed === void 0 || notRun === void 0 || planned.length === 0 || passed.length === 0 || failed.length === 0 || !unique(planned) || !unique(passed) || !unique(failed) || !unique(notRun)) {
|
|
2201
|
+
return void 0;
|
|
2202
|
+
}
|
|
2203
|
+
const passedSet = new Set(passed);
|
|
2204
|
+
const failedSet = new Set(failed);
|
|
2205
|
+
const notRunSet = new Set(notRun);
|
|
2206
|
+
const partitionIsExact = planned.every((label) => {
|
|
2207
|
+
const memberships = Number(passedSet.has(label)) + Number(failedSet.has(label)) + Number(notRunSet.has(label));
|
|
2208
|
+
return memberships === 1;
|
|
2209
|
+
});
|
|
2210
|
+
const noUnknownLabels = [...passed, ...failed, ...notRun].every(
|
|
2211
|
+
(label) => planned.includes(label)
|
|
2212
|
+
);
|
|
2213
|
+
if (!partitionIsExact || !noUnknownLabels) return void 0;
|
|
2214
|
+
return {
|
|
2215
|
+
sourceSha: source.sha,
|
|
2216
|
+
commandIndex: failedCommandIndex,
|
|
2217
|
+
manifestDigest: evidence.manifestDigest,
|
|
2218
|
+
rerunLabels: planned.filter((label) => failedSet.has(label) || notRunSet.has(label)),
|
|
2219
|
+
reusedLabels: planned.filter((label) => passedSet.has(label))
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
function verifyGateRetryPass(plan, output) {
|
|
2223
|
+
const evidence = parseGateEvidence(output);
|
|
2224
|
+
if (evidence?.outcome !== "pass" || evidence.manifestDigest !== plan.manifestDigest || evidence.failedLabels === void 0 || evidence.notRunLabels === void 0 || evidence.passedLabels === void 0 || evidence.failedLabels.length > 0 || evidence.notRunLabels.length > 0) {
|
|
2225
|
+
return { valid: false, applied: false };
|
|
2226
|
+
}
|
|
2227
|
+
if (evidence.retryApplied === true) {
|
|
2228
|
+
const valid2 = evidence.retrySourceManifestDigest === plan.manifestDigest && sameOrderedValues(evidence.retryReusedLabels ?? [], plan.reusedLabels) && sameOrderedValues(evidence.retryRerunLabels ?? [], plan.rerunLabels) && sameOrderedValues(evidence.plannedLabels ?? [], plan.rerunLabels) && sameOrderedValues(evidence.passedLabels, plan.rerunLabels);
|
|
2229
|
+
return { valid: valid2, applied: valid2 };
|
|
2230
|
+
}
|
|
2231
|
+
const fullLabels = [...plan.reusedLabels, ...plan.rerunLabels];
|
|
2232
|
+
const planned = evidence.plannedLabels ?? [];
|
|
2233
|
+
const valid = planned.length === fullLabels.length && fullLabels.every((label) => planned.includes(label)) && planned.every((label) => fullLabels.includes(label)) && sameOrderedValues(evidence.passedLabels, planned);
|
|
2234
|
+
return { valid, applied: false };
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/core/ci/local-ci-worker.ts
|
|
2238
|
+
function message(error) {
|
|
2239
|
+
return error instanceof Error ? error.message : String(error);
|
|
2240
|
+
}
|
|
2241
|
+
function updated(record, deps, patch) {
|
|
2242
|
+
return {
|
|
2243
|
+
...record,
|
|
2244
|
+
...patch,
|
|
2245
|
+
updatedAt: deps.now().toISOString()
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
async function save(record, deps) {
|
|
2249
|
+
return deps.saveRecord(record);
|
|
2250
|
+
}
|
|
2251
|
+
async function recordCompleteCommandOutput(deps, input) {
|
|
2252
|
+
try {
|
|
2253
|
+
await deps.recordCommandOutput?.(input);
|
|
2254
|
+
} catch {
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
async function persistFailureObligation(record, deps, error, publicationDescription = error, failureClass) {
|
|
2258
|
+
const failed = updated(record, deps, {
|
|
2259
|
+
status: "failed",
|
|
2260
|
+
error,
|
|
2261
|
+
...failureClass ? { failureClass } : {},
|
|
2262
|
+
outstandingRemoteStatusPublication: createOutstandingFailureStatusPublication(
|
|
2263
|
+
record,
|
|
2264
|
+
publicationDescription
|
|
2265
|
+
)
|
|
2266
|
+
});
|
|
2267
|
+
try {
|
|
2268
|
+
return await save(failed, deps);
|
|
2269
|
+
} catch {
|
|
2270
|
+
return failed;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
var MAX_FAILING_TESTS = 50;
|
|
2274
|
+
function extractFailingTests(step, output) {
|
|
2275
|
+
const kind = describeVerificationCommand(step.command).kind;
|
|
2276
|
+
if (kind !== "test" && kind !== "unknown") {
|
|
2277
|
+
return { failingTests: [], truncated: false };
|
|
2278
|
+
}
|
|
2279
|
+
const phase = step.phase ?? "verify";
|
|
2280
|
+
const found = readFailingTestNames(step.command, output);
|
|
2281
|
+
const identifiers = [...new Set(found?.located ?? found?.names ?? [])];
|
|
2282
|
+
const failingTests = identifiers.slice(0, MAX_FAILING_TESTS).map((identifier) => {
|
|
2283
|
+
const { file, testPath } = splitTestFailureId(identifier);
|
|
2284
|
+
return { ...file !== void 0 ? { file } : {}, test: testPath, phase };
|
|
2285
|
+
});
|
|
2286
|
+
return { failingTests, truncated: identifiers.length > MAX_FAILING_TESTS };
|
|
2287
|
+
}
|
|
2288
|
+
function resolveExecutableSteps(authority) {
|
|
2289
|
+
const planSteps = authority.verificationPlan?.steps;
|
|
2290
|
+
if (planSteps && planSteps.length > 0) {
|
|
2291
|
+
return planSteps.map((step) => ({
|
|
2292
|
+
id: step.id,
|
|
2293
|
+
phase: step.phase,
|
|
2294
|
+
command: step.command,
|
|
2295
|
+
...step.timeoutMs !== void 0 ? { timeoutMs: step.timeoutMs } : {}
|
|
2296
|
+
}));
|
|
2297
|
+
}
|
|
2298
|
+
return (authority.commands ?? []).map((command) => ({ command }));
|
|
2299
|
+
}
|
|
2300
|
+
function computeQueueWaitMs(createdAt, startedAt) {
|
|
2301
|
+
const createdMs = Date.parse(createdAt);
|
|
2302
|
+
if (!Number.isFinite(createdMs)) return 0;
|
|
2303
|
+
return Math.max(0, startedAt.getTime() - createdMs);
|
|
2304
|
+
}
|
|
2305
|
+
async function releaseScarceResources(record, deps, resources) {
|
|
2306
|
+
let next = record;
|
|
2307
|
+
if (resources.worktreePath) {
|
|
2308
|
+
const pathToRemove = resources.worktreePath;
|
|
2309
|
+
try {
|
|
2310
|
+
await deps.removeWorktree(next.repoRoot, pathToRemove);
|
|
2311
|
+
resources.worktreePath = void 0;
|
|
2312
|
+
if (next.worktreePath) {
|
|
2313
|
+
try {
|
|
2314
|
+
next = await save(updated(next, deps, { worktreePath: void 0 }), deps);
|
|
2315
|
+
} catch {
|
|
2316
|
+
next = { ...next, worktreePath: void 0 };
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
} catch (error) {
|
|
2320
|
+
const detail = message(error);
|
|
2321
|
+
const cleanupNote = `Worktree cleanup pending: ${detail}`;
|
|
2322
|
+
try {
|
|
2323
|
+
next = await save(
|
|
2324
|
+
updated(next, deps, {
|
|
2325
|
+
worktreePath: next.worktreePath ?? pathToRemove,
|
|
2326
|
+
// Preserve validation/failure error; surface cleanup only when empty.
|
|
2327
|
+
error: next.error ?? cleanupNote
|
|
2328
|
+
}),
|
|
2329
|
+
deps
|
|
2330
|
+
);
|
|
2331
|
+
} catch {
|
|
2332
|
+
next = {
|
|
2333
|
+
...next,
|
|
2334
|
+
worktreePath: next.worktreePath ?? pathToRemove,
|
|
2335
|
+
error: next.error ?? cleanupNote
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (resources.slot) {
|
|
2341
|
+
await resources.slot.release().catch(() => void 0);
|
|
2342
|
+
resources.slot = void 0;
|
|
2343
|
+
}
|
|
2344
|
+
return next;
|
|
2345
|
+
}
|
|
2346
|
+
async function awaitAdvisoryPending(resources) {
|
|
2347
|
+
if (resources.pendingStatus) {
|
|
2348
|
+
await resources.pendingStatus;
|
|
2349
|
+
resources.pendingStatus = void 0;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function preservedDispositionError(record) {
|
|
2353
|
+
return record.error?.startsWith("Worktree cleanup pending:") ? record.error : void 0;
|
|
2354
|
+
}
|
|
2355
|
+
async function publishAfterRelease(record, deps, resources) {
|
|
2356
|
+
let next = await releaseScarceResources(record, deps, resources);
|
|
2357
|
+
await awaitAdvisoryPending(resources);
|
|
2358
|
+
if (!hasOutstandingRemoteStatusPublication(next)) {
|
|
2359
|
+
return next;
|
|
2360
|
+
}
|
|
2361
|
+
try {
|
|
2362
|
+
next = await deps.publishOutstandingStatus(next);
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
const detail = message(error);
|
|
2365
|
+
if (next.status === "publication_pending") {
|
|
2366
|
+
try {
|
|
2367
|
+
next = await save(
|
|
2368
|
+
updated(next, deps, {
|
|
2369
|
+
error: `Remote status publication pending: ${detail}`
|
|
2370
|
+
}),
|
|
2371
|
+
deps
|
|
2372
|
+
);
|
|
2373
|
+
} catch {
|
|
2374
|
+
next = {
|
|
2375
|
+
...next,
|
|
2376
|
+
error: `Remote status publication pending: ${detail}`
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return next;
|
|
2381
|
+
}
|
|
2382
|
+
if (next.status === "publication_pending" && hasOutstandingRemoteStatusPublication(next) && !next.error) {
|
|
2383
|
+
try {
|
|
2384
|
+
next = await save(
|
|
2385
|
+
updated(next, deps, {
|
|
2386
|
+
error: "Remote status publication pending; reconcile will retry."
|
|
2387
|
+
}),
|
|
2388
|
+
deps
|
|
2389
|
+
);
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return next;
|
|
2394
|
+
}
|
|
2395
|
+
async function persistAndPublishDisposition(record, deps, resources, input) {
|
|
2396
|
+
const cleanupError = preservedDispositionError(record);
|
|
2397
|
+
const dispositionError = cleanupError ?? input.error;
|
|
2398
|
+
const publicationPending = updated(record, deps, {
|
|
2399
|
+
status: "publication_pending",
|
|
2400
|
+
error: dispositionError,
|
|
2401
|
+
outstandingRemoteStatusPublication: createOutstandingStatusPublication(record, {
|
|
2402
|
+
state: input.state,
|
|
2403
|
+
description: input.description,
|
|
2404
|
+
targetStatus: input.targetStatus
|
|
2405
|
+
})
|
|
2406
|
+
});
|
|
2407
|
+
let next;
|
|
2408
|
+
try {
|
|
2409
|
+
next = await save(publicationPending, deps);
|
|
2410
|
+
} catch {
|
|
2411
|
+
return publicationPending;
|
|
2412
|
+
}
|
|
2413
|
+
next = await publishAfterRelease(next, deps, resources);
|
|
2414
|
+
if (next.status === input.targetStatus && !hasOutstandingRemoteStatusPublication(next) && input.error !== void 0) {
|
|
2415
|
+
try {
|
|
2416
|
+
next = await save(
|
|
2417
|
+
updated(next, deps, {
|
|
2418
|
+
error: input.error
|
|
2419
|
+
}),
|
|
2420
|
+
deps
|
|
2421
|
+
);
|
|
2422
|
+
} catch {
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return next;
|
|
2426
|
+
}
|
|
2427
|
+
async function maybeSupersede(record, deps) {
|
|
2428
|
+
if (!deps.hasNewerSubmission) return null;
|
|
2429
|
+
const newerExists = await deps.hasNewerSubmission({
|
|
2430
|
+
repository: record.repository,
|
|
2431
|
+
branch: record.branch,
|
|
2432
|
+
sha: record.sha,
|
|
2433
|
+
createdAt: record.createdAt,
|
|
2434
|
+
submittedAt: record.submittedAt
|
|
2435
|
+
});
|
|
2436
|
+
if (!newerExists) return null;
|
|
2437
|
+
return save(
|
|
2438
|
+
updated(record, deps, {
|
|
2439
|
+
status: "superseded",
|
|
2440
|
+
error: "Superseded by a newer submission for this branch."
|
|
2441
|
+
}),
|
|
2442
|
+
deps
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
async function supersedeCancelledRun(record, deps, resources, processGroupSurvived = false) {
|
|
2446
|
+
const reason = deps.signal?.reason;
|
|
2447
|
+
const base = reason instanceof Error ? reason.message : "the run was cancelled";
|
|
2448
|
+
const detail = processGroupSurvived ? `${base} Its process group outlived the cancellation wait; worktree retained for cleanup.` : base;
|
|
2449
|
+
if (processGroupSurvived) resources.worktreePath = void 0;
|
|
2450
|
+
const released = await releaseScarceResources(record, deps, resources);
|
|
2451
|
+
await awaitAdvisoryPending(resources);
|
|
2452
|
+
return save(
|
|
2453
|
+
updated(released, deps, {
|
|
2454
|
+
status: "superseded",
|
|
2455
|
+
error: `Local validation cancelled: ${detail}`
|
|
2456
|
+
}),
|
|
2457
|
+
deps
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
async function executeLocalCiWorker(initialRecord, deps) {
|
|
2461
|
+
let record = initialRecord;
|
|
2462
|
+
const resources = {
|
|
2463
|
+
worktreePath: void 0,
|
|
2464
|
+
slot: void 0,
|
|
2465
|
+
pendingStatus: void 0
|
|
2466
|
+
};
|
|
2467
|
+
let materializing = true;
|
|
2468
|
+
try {
|
|
2469
|
+
const preSlotSuperseded = await maybeSupersede(record, deps);
|
|
2470
|
+
if (preSlotSuperseded) return preSlotSuperseded;
|
|
2471
|
+
resources.slot = await deps.acquireSlot();
|
|
2472
|
+
const startedAt = deps.now();
|
|
2473
|
+
const startedAtIso = startedAt.toISOString();
|
|
2474
|
+
record = await save(
|
|
2475
|
+
updated(record, deps, {
|
|
2476
|
+
status: "running",
|
|
2477
|
+
pid: process.pid,
|
|
2478
|
+
error: void 0,
|
|
2479
|
+
startedAt: startedAtIso,
|
|
2480
|
+
queueWaitMs: computeQueueWaitMs(record.createdAt, startedAt)
|
|
2481
|
+
}),
|
|
2482
|
+
deps
|
|
2483
|
+
);
|
|
2484
|
+
const postSlotSuperseded = await maybeSupersede(record, deps);
|
|
2485
|
+
if (postSlotSuperseded) return postSlotSuperseded;
|
|
2486
|
+
resources.worktreePath = await deps.createWorktree(record.repoRoot, record.sha);
|
|
2487
|
+
record = await save(updated(record, deps, { worktreePath: resources.worktreePath }), deps);
|
|
2488
|
+
const executedTree = await (deps.resolveExecutedTree ?? resolveExecutedTree)({
|
|
2489
|
+
repoRoot: resources.worktreePath,
|
|
2490
|
+
candidateSha: "HEAD"
|
|
2491
|
+
});
|
|
2492
|
+
if (executedTree === void 0) {
|
|
2493
|
+
const error = "Local validation failed: executed tree could not be resolved";
|
|
2494
|
+
record = await persistFailureObligation(record, deps, error, error, "infrastructure");
|
|
2495
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2496
|
+
}
|
|
2497
|
+
materializing = false;
|
|
2498
|
+
const authority = await deps.resolveAuthority(resources.worktreePath);
|
|
2499
|
+
record = await save(
|
|
2500
|
+
updated(record, deps, {
|
|
2501
|
+
statusContext: authority.statusContext,
|
|
2502
|
+
maxConcurrent: authority.maxConcurrent,
|
|
2503
|
+
trustedBaseRef: authority.trustedBaseRef,
|
|
2504
|
+
trustedBaseSha: authority.trustedBaseSha,
|
|
2505
|
+
pullRequestNumber: authority.pullRequest?.number,
|
|
2506
|
+
validationProfile: authority.validationProfile,
|
|
2507
|
+
entrypointFingerprint: authority.commandEntrypoint.entrypointFingerprint,
|
|
2508
|
+
validationProfileReason: boundLocalCiValidationProfileReason(
|
|
2509
|
+
authority.validationProfileReason
|
|
2510
|
+
)
|
|
2511
|
+
}),
|
|
2512
|
+
deps
|
|
2513
|
+
);
|
|
2514
|
+
if (authority.trusted && authority.trustedBaseSha && deps.refreshSchedulingEnvelope) {
|
|
2515
|
+
await deps.refreshSchedulingEnvelope({
|
|
2516
|
+
repository: record.repository,
|
|
2517
|
+
maxConcurrent: authority.maxConcurrent,
|
|
2518
|
+
trustedBaseSha: authority.trustedBaseSha
|
|
2519
|
+
}).catch(() => void 0);
|
|
2520
|
+
}
|
|
2521
|
+
if (!authority.enabled) {
|
|
2522
|
+
return save(
|
|
2523
|
+
updated(record, deps, {
|
|
2524
|
+
status: "skipped",
|
|
2525
|
+
error: "Local CI disabled by trusted base policy."
|
|
2526
|
+
}),
|
|
2527
|
+
deps
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
if (!authority.trusted || !authority.trustedBaseSha) {
|
|
2531
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2532
|
+
state: "pending",
|
|
2533
|
+
description: "Local validation deferred; trusted-base authority pending",
|
|
2534
|
+
targetStatus: "succeeded_pending",
|
|
2535
|
+
error: `Success attestation pending trusted-base authority: ${authority.validationProfileReason}`
|
|
2536
|
+
});
|
|
2537
|
+
return record;
|
|
2538
|
+
}
|
|
2539
|
+
resources.pendingStatus = deps.publishStatus(
|
|
2540
|
+
{
|
|
2541
|
+
repository: record.repository,
|
|
2542
|
+
sha: record.sha,
|
|
2543
|
+
context: record.statusContext,
|
|
2544
|
+
state: "pending",
|
|
2545
|
+
description: "Local validation queued"
|
|
2546
|
+
},
|
|
2547
|
+
{ attempts: 1 }
|
|
2548
|
+
).catch(() => void 0);
|
|
2549
|
+
const steps = resolveExecutableSteps(authority);
|
|
2550
|
+
const structuredPlanSteps = authority.verificationPlan?.steps;
|
|
2551
|
+
const unavailable = steps.length === 0 ? "no deterministic project command resolved." : structuredPlanSteps !== void 0 && structuredPlanSteps.length > 0 && !structuredPlanSteps.some((step) => step.phase === "verify") ? "structured plan has no verify step." : void 0;
|
|
2552
|
+
if (unavailable) {
|
|
2553
|
+
const error = `Verification unavailable: ${unavailable}`;
|
|
2554
|
+
record = await persistFailureObligation(record, deps, error);
|
|
2555
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2556
|
+
}
|
|
2557
|
+
const entrypointBlockedReason = authority.commandEntrypoint.blockedReason;
|
|
2558
|
+
if (entrypointBlockedReason) {
|
|
2559
|
+
record = await save(
|
|
2560
|
+
updated(record, deps, {
|
|
2561
|
+
entrypointApprovalPending: true
|
|
2562
|
+
}),
|
|
2563
|
+
deps
|
|
2564
|
+
);
|
|
2565
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2566
|
+
state: "pending",
|
|
2567
|
+
description: "Trusted verification entrypoint change requires review",
|
|
2568
|
+
targetStatus: "succeeded_pending",
|
|
2569
|
+
error: `Success attestation pending trusted verification entrypoint: ${entrypointBlockedReason}. Review the change, then rerun with BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA=${record.sha}.`
|
|
2570
|
+
});
|
|
2571
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2572
|
+
}
|
|
2573
|
+
let gateRetryPlan;
|
|
2574
|
+
if (record.retryOfSha !== void 0 && deps.loadRecord !== void 0) {
|
|
2575
|
+
try {
|
|
2576
|
+
const source = await deps.loadRecord(record.repository, record.retryOfSha);
|
|
2577
|
+
gateRetryPlan = resolveGateRetryPlan({
|
|
2578
|
+
current: record,
|
|
2579
|
+
source,
|
|
2580
|
+
trustedBaseSha: authority.trustedBaseSha,
|
|
2581
|
+
validationProfile: authority.validationProfile,
|
|
2582
|
+
steps
|
|
2583
|
+
});
|
|
2584
|
+
if (gateRetryPlan !== void 0) {
|
|
2585
|
+
record = await save(
|
|
2586
|
+
updated(record, deps, {
|
|
2587
|
+
gateRetry: {
|
|
2588
|
+
sourceSha: gateRetryPlan.sourceSha,
|
|
2589
|
+
manifestDigest: gateRetryPlan.manifestDigest,
|
|
2590
|
+
rerunLabels: gateRetryPlan.rerunLabels,
|
|
2591
|
+
reusedLabels: gateRetryPlan.reusedLabels
|
|
2592
|
+
}
|
|
2593
|
+
}),
|
|
2594
|
+
deps
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const commandResults = [];
|
|
2601
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
2602
|
+
if (deps.signal?.aborted) return await supersedeCancelledRun(record, deps, resources);
|
|
2603
|
+
const stepStartedMs = deps.now().getTime();
|
|
2604
|
+
const runOptions = {
|
|
2605
|
+
...step.timeoutMs !== void 0 ? { timeoutMs: step.timeoutMs } : {},
|
|
2606
|
+
...authority.trustedBaseSha ? { trustedBaseSha: authority.trustedBaseSha } : {},
|
|
2607
|
+
// Authority-derived only; runtime strips ambient BUILD_LOCAL_CI_PROFILE.
|
|
2608
|
+
validationProfile: authority.validationProfile,
|
|
2609
|
+
...gateRetryPlan?.commandIndex === stepIndex ? {
|
|
2610
|
+
gateRetry: {
|
|
2611
|
+
sourceSha: gateRetryPlan.sourceSha,
|
|
2612
|
+
manifestDigest: gateRetryPlan.manifestDigest,
|
|
2613
|
+
rerunLabels: gateRetryPlan.rerunLabels,
|
|
2614
|
+
reusedLabels: gateRetryPlan.reusedLabels
|
|
2615
|
+
}
|
|
2616
|
+
} : {}
|
|
2617
|
+
};
|
|
2618
|
+
const result = await deps.runCommand(
|
|
2619
|
+
step.command,
|
|
2620
|
+
resources.worktreePath,
|
|
2621
|
+
Object.keys(runOptions).length > 0 ? runOptions : void 0
|
|
2622
|
+
);
|
|
2623
|
+
if (deps.signal?.aborted) {
|
|
2624
|
+
return await supersedeCancelledRun(record, deps, resources, result.processGroupSurvived);
|
|
2625
|
+
}
|
|
2626
|
+
if (gateRetryPlan?.commandIndex === stepIndex && result.exitCode === 0) {
|
|
2627
|
+
const verification = verifyGateRetryPass(gateRetryPlan, result.output);
|
|
2628
|
+
if (!verification.valid) {
|
|
2629
|
+
await recordCompleteCommandOutput(deps, {
|
|
2630
|
+
command: step.command,
|
|
2631
|
+
...step.id !== void 0 ? { id: step.id } : {},
|
|
2632
|
+
...step.phase !== void 0 ? { phase: step.phase } : {},
|
|
2633
|
+
output: result.output
|
|
2634
|
+
});
|
|
2635
|
+
const commandResult2 = boundLocalCiCommandOutput({
|
|
2636
|
+
command: step.command,
|
|
2637
|
+
exitCode: 1,
|
|
2638
|
+
output: result.output,
|
|
2639
|
+
...step.id !== void 0 ? { id: step.id } : {},
|
|
2640
|
+
...step.phase !== void 0 ? { phase: step.phase } : {}
|
|
2641
|
+
});
|
|
2642
|
+
commandResults.push(commandResult2);
|
|
2643
|
+
record = await save(updated(record, deps, { commands: [...commandResults] }), deps);
|
|
2644
|
+
record = await persistFailureObligation(
|
|
2645
|
+
record,
|
|
2646
|
+
deps,
|
|
2647
|
+
"Local validation failed: tranche retry evidence was incomplete or inconsistent"
|
|
2648
|
+
);
|
|
2649
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2650
|
+
}
|
|
2651
|
+
record = await save(
|
|
2652
|
+
updated(record, deps, {
|
|
2653
|
+
gateRetry: {
|
|
2654
|
+
sourceSha: gateRetryPlan.sourceSha,
|
|
2655
|
+
manifestDigest: gateRetryPlan.manifestDigest,
|
|
2656
|
+
rerunLabels: gateRetryPlan.rerunLabels,
|
|
2657
|
+
reusedLabels: gateRetryPlan.reusedLabels,
|
|
2658
|
+
applied: verification.applied
|
|
2659
|
+
}
|
|
2660
|
+
}),
|
|
2661
|
+
deps
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
const durationMs = Math.max(0, deps.now().getTime() - stepStartedMs);
|
|
2665
|
+
await recordCompleteCommandOutput(deps, {
|
|
2666
|
+
command: step.command,
|
|
2667
|
+
...step.id !== void 0 ? { id: step.id } : {},
|
|
2668
|
+
...step.phase !== void 0 ? { phase: step.phase } : {},
|
|
2669
|
+
output: result.output
|
|
2670
|
+
});
|
|
2671
|
+
const commandResult = boundLocalCiCommandOutput({
|
|
2672
|
+
command: step.command,
|
|
2673
|
+
exitCode: result.exitCode,
|
|
2674
|
+
output: result.output,
|
|
2675
|
+
durationMs,
|
|
2676
|
+
...step.id !== void 0 ? { id: step.id } : {},
|
|
2677
|
+
...step.phase !== void 0 ? { phase: step.phase } : {}
|
|
2678
|
+
});
|
|
2679
|
+
commandResults.push(commandResult);
|
|
2680
|
+
record = await save(updated(record, deps, { commands: [...commandResults] }), deps);
|
|
2681
|
+
const emptyRun = detectEmptyRun(step.command, result.output, result.exitCode);
|
|
2682
|
+
if (emptyRun && result.exitCode === 0 && step.phase !== "setup") {
|
|
2683
|
+
record = await persistFailureObligation(
|
|
2684
|
+
record,
|
|
2685
|
+
deps,
|
|
2686
|
+
`Local validation failed: ${step.command} collected or ran zero tests`
|
|
2687
|
+
);
|
|
2688
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2689
|
+
}
|
|
2690
|
+
if (result.exitCode !== 0 || step.phase !== "setup" && detectExitZeroVerifyFailure(step.command, result.output)) {
|
|
2691
|
+
const parsed = extractFailingTests(step, result.output);
|
|
2692
|
+
if (parsed.failingTests.length > 0) {
|
|
2693
|
+
record = updated(record, deps, {
|
|
2694
|
+
failingTests: parsed.failingTests,
|
|
2695
|
+
...parsed.truncated ? { failingTestsTruncated: true } : {}
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
const gateEvidence = parseGateEvidence(result.output);
|
|
2699
|
+
const reason = emptyRun ? ` ran no tests (${emptyRun.reason})` : "";
|
|
2700
|
+
const error = `${describeGateFailure(gateEvidence) ?? `Local validation failed: ${step.command}${reason}`}${describeUnavailableDependencySetup(authority.setupUnavailable)}`;
|
|
2701
|
+
record = await persistFailureObligation(record, deps, error);
|
|
2702
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
const refusal = !await (deps.worktreeMatchesExecutedTree ?? worktreeMatchesExecutedTree)({
|
|
2706
|
+
worktreePath: resources.worktreePath,
|
|
2707
|
+
executedTree
|
|
2708
|
+
}) ? "Local validation failed: worktree no longer matches the executed tree" : authority.coverageGap && `Verification unavailable: ${authority.coverageGap}`;
|
|
2709
|
+
if (refusal) {
|
|
2710
|
+
record = await persistFailureObligation(record, deps, refusal);
|
|
2711
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2712
|
+
}
|
|
2713
|
+
record = await releaseScarceResources(record, deps, resources);
|
|
2714
|
+
await awaitAdvisoryPending(resources);
|
|
2715
|
+
let pullRequest;
|
|
2716
|
+
try {
|
|
2717
|
+
pullRequest = await deps.findPullRequest({
|
|
2718
|
+
repository: record.repository,
|
|
2719
|
+
branch: record.branch,
|
|
2720
|
+
number: authority.pullRequest?.number,
|
|
2721
|
+
base: authority.trustedBaseRef
|
|
2722
|
+
});
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2725
|
+
state: "pending",
|
|
2726
|
+
description: "Local validation passed; pull request visibility pending",
|
|
2727
|
+
targetStatus: "succeeded_pending",
|
|
2728
|
+
error: `Success attestation pending pull request visibility: ${message(error)}`
|
|
2729
|
+
});
|
|
2730
|
+
return record;
|
|
2731
|
+
}
|
|
2732
|
+
if (!pullRequest || pullRequest.draft) {
|
|
2733
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2734
|
+
state: "pending",
|
|
2735
|
+
description: "Local validation passed; pull request visibility pending",
|
|
2736
|
+
targetStatus: "succeeded_pending",
|
|
2737
|
+
error: !pullRequest ? "Success attestation pending pull request visibility." : "Success attestation pending non-draft pull request."
|
|
2738
|
+
});
|
|
2739
|
+
return record;
|
|
2740
|
+
}
|
|
2741
|
+
if (pullRequest.headSha.toLowerCase() !== record.sha.toLowerCase()) {
|
|
2742
|
+
return save(
|
|
2743
|
+
updated(record, deps, {
|
|
2744
|
+
status: "superseded",
|
|
2745
|
+
error: "Pull request head changed after local validation."
|
|
2746
|
+
}),
|
|
2747
|
+
deps
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
if (record.pullRequestNumber !== pullRequest.number) {
|
|
2751
|
+
record = await save(updated(record, deps, { pullRequestNumber: pullRequest.number }), deps);
|
|
2752
|
+
}
|
|
2753
|
+
const trustedBaseSha = authority.trustedBaseSha;
|
|
2754
|
+
if (!pullRequest.baseSha) {
|
|
2755
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2756
|
+
state: "pending",
|
|
2757
|
+
description: "Local validation passed; trusted PR base pending",
|
|
2758
|
+
targetStatus: "succeeded_pending",
|
|
2759
|
+
error: "Success attestation pending trusted PR base: PR base SHA is missing."
|
|
2760
|
+
});
|
|
2761
|
+
return record;
|
|
2762
|
+
}
|
|
2763
|
+
if (pullRequest.baseSha.toLowerCase() !== trustedBaseSha.toLowerCase()) {
|
|
2764
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2765
|
+
state: "pending",
|
|
2766
|
+
description: "Local validation passed; trusted PR base pending",
|
|
2767
|
+
targetStatus: "succeeded_pending",
|
|
2768
|
+
error: `Success attestation pending trusted PR base: validated ${trustedBaseSha.slice(0, 12)}, observed ${pullRequest.baseSha.slice(0, 12)}; run build ci reconcile.`
|
|
2769
|
+
});
|
|
2770
|
+
return record;
|
|
2771
|
+
}
|
|
2772
|
+
if (pullRequest.baseRefName !== authority.trustedBaseRef) {
|
|
2773
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2774
|
+
state: "pending",
|
|
2775
|
+
description: "Local validation passed; trusted PR base pending",
|
|
2776
|
+
targetStatus: "succeeded_pending",
|
|
2777
|
+
error: `Success attestation pending trusted PR base: base ref changed to ${pullRequest.baseRefName ?? "unknown"}.`
|
|
2778
|
+
});
|
|
2779
|
+
return record;
|
|
2780
|
+
}
|
|
2781
|
+
const description = `Local validation passed (${steps.length} command${steps.length === 1 ? "" : "s"})`;
|
|
2782
|
+
record = await persistAndPublishDisposition(record, deps, resources, {
|
|
2783
|
+
state: "success",
|
|
2784
|
+
description,
|
|
2785
|
+
targetStatus: "succeeded"
|
|
2786
|
+
});
|
|
2787
|
+
return record;
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
if (deps.signal?.aborted && !materializing) {
|
|
2790
|
+
return await supersedeCancelledRun(record, deps, resources);
|
|
2791
|
+
}
|
|
2792
|
+
const detail = message(error);
|
|
2793
|
+
record = await persistFailureObligation(
|
|
2794
|
+
record,
|
|
2795
|
+
deps,
|
|
2796
|
+
detail,
|
|
2797
|
+
`Local validation error: ${detail}`,
|
|
2798
|
+
materializing ? "infrastructure" : void 0
|
|
2799
|
+
);
|
|
2800
|
+
return await publishAfterRelease(record, deps, resources);
|
|
2801
|
+
} finally {
|
|
2802
|
+
if (resources.worktreePath) {
|
|
2803
|
+
await deps.removeWorktree(record.repoRoot, resources.worktreePath).catch(() => void 0);
|
|
2804
|
+
resources.worktreePath = void 0;
|
|
2805
|
+
}
|
|
2806
|
+
if (resources.slot) {
|
|
2807
|
+
await resources.slot.release().catch(() => void 0);
|
|
2808
|
+
resources.slot = void 0;
|
|
2809
|
+
}
|
|
2810
|
+
if (resources.pendingStatus) {
|
|
2811
|
+
await resources.pendingStatus;
|
|
2812
|
+
resources.pendingStatus = void 0;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/core/ci/local-ci-execution.ts
|
|
2818
|
+
async function executeClaimedLocalCiRecord(input) {
|
|
2819
|
+
const { record, store } = input;
|
|
2820
|
+
if (input.manageProcess !== false) keepMacAwakeForCurrentProcess();
|
|
2821
|
+
const heartbeat = input.manageProcess === false ? void 0 : await startDetachedWorkerRegistryHeartbeat({
|
|
2822
|
+
taskId: `local-ci-${record.sha.slice(0, 12)}`,
|
|
2823
|
+
command: "build ci worker",
|
|
2824
|
+
owner: "local-ci",
|
|
2825
|
+
metadata: { repository: record.repository, sha: record.sha }
|
|
2826
|
+
});
|
|
2827
|
+
let retainedLogBytes = 0;
|
|
2828
|
+
try {
|
|
2829
|
+
retainedLogBytes = statSync(store.logPath(record.repository, record.sha)).size;
|
|
2830
|
+
} catch {
|
|
2831
|
+
}
|
|
2832
|
+
try {
|
|
2833
|
+
const completed = await executeLocalCiWorker(record, {
|
|
2834
|
+
acquireSlot: () => input.slot ? Promise.resolve({ slot: input.slot.slot, release: async () => void 0 }) : acquireLocalCiSlot({ repository: record.repository, stateRoot: store.root }),
|
|
2835
|
+
createWorktree: createLocalCiWorktree,
|
|
2836
|
+
removeWorktree: removeLocalCiWorktree,
|
|
2837
|
+
resolveAuthority: (worktreePath) => resolveLocalCiAuthority(record, worktreePath, record.repoRoot, {}, input.trust),
|
|
2838
|
+
runCommand: (command, cwd, options) => runLocalCiVerificationCommand(command, cwd, { ...options, signal: input.signal }),
|
|
2839
|
+
signal: input.signal,
|
|
2840
|
+
recordCommandOutput: ({ command, id, phase, output }) => {
|
|
2841
|
+
const identity = [phase, id].filter(Boolean).join("/");
|
|
2842
|
+
const entry = renderBoundedLocalCiLogEntry({
|
|
2843
|
+
header: `[local-ci] command output attempt=${record.createdAt} ${identity ? `${identity}: ${command}` : command}`,
|
|
2844
|
+
output,
|
|
2845
|
+
maxBytes: Math.max(0, MAX_LOCAL_CI_RETAINED_LOG_BYTES - retainedLogBytes)
|
|
2846
|
+
});
|
|
2847
|
+
if (!entry.length) return;
|
|
2848
|
+
if (input.manageProcess === false) {
|
|
2849
|
+
appendFileSync(store.logPath(record.repository, record.sha), entry, { mode: 384 });
|
|
2850
|
+
} else {
|
|
2851
|
+
process.stdout.write(entry);
|
|
2852
|
+
}
|
|
2853
|
+
retainedLogBytes += Buffer.byteLength(entry, "utf8");
|
|
2854
|
+
},
|
|
2855
|
+
loadRecord: (repository, sourceSha) => store.read(repository, sourceSha),
|
|
2856
|
+
hasNewerSubmission: (value) => store.hasNewerSubmission(value),
|
|
2857
|
+
refreshSchedulingEnvelope: async ({ repository, maxConcurrent, trustedBaseSha }) => {
|
|
2858
|
+
await writeSchedulingEnvelope({
|
|
2859
|
+
repository,
|
|
2860
|
+
maxConcurrent,
|
|
2861
|
+
updatedFromBaseSha: trustedBaseSha,
|
|
2862
|
+
stateRoot: store.root
|
|
2863
|
+
});
|
|
2864
|
+
},
|
|
2865
|
+
publishStatus: publishGitHubLocalCiStatus,
|
|
2866
|
+
findPullRequest: (value) => findGitHubPullRequest(value),
|
|
2867
|
+
saveRecord: (value) => store.save(value),
|
|
2868
|
+
publishOutstandingStatus: input.publishOutstandingStatus,
|
|
2869
|
+
now: () => /* @__PURE__ */ new Date()
|
|
2870
|
+
});
|
|
2871
|
+
if (completed.status !== "queued" && completed.status !== "running") {
|
|
2872
|
+
await appendLandingLogEntry(store.root, "gate", gateLandingFields(completed));
|
|
2873
|
+
}
|
|
2874
|
+
if (input.manageProcess === false && completed.status !== "queued" && completed.status !== "running") {
|
|
2875
|
+
return (await store.compareAndSave(completed, {
|
|
2876
|
+
...completed,
|
|
2877
|
+
pid: void 0,
|
|
2878
|
+
pidIdentity: void 0
|
|
2879
|
+
})).record;
|
|
2880
|
+
}
|
|
2881
|
+
return completed;
|
|
2882
|
+
} finally {
|
|
2883
|
+
await heartbeat?.unregister();
|
|
2884
|
+
await store.releaseWorkerClaim(record.repository, record.sha, process.pid).catch(() => void 0);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/core/ci/local-ci-recovery-activity.ts
|
|
2889
|
+
async function readRemoteBranchExists(repoRoot, remote, branch) {
|
|
2890
|
+
const ref = `refs/heads/${branch}`;
|
|
2891
|
+
try {
|
|
2892
|
+
const valid = await execGit(["check-ref-format", ref], { cwd: repoRoot, timeoutMs: 1e4 });
|
|
2893
|
+
if (valid.exitCode !== 0) return null;
|
|
2894
|
+
const result = await execGit(["ls-remote", "--refs", "--", remote, ref], {
|
|
2895
|
+
cwd: repoRoot,
|
|
2896
|
+
timeoutMs: 1e4,
|
|
2897
|
+
stripFinalNewline: false
|
|
2898
|
+
});
|
|
2899
|
+
if (result.exitCode !== 0) return null;
|
|
2900
|
+
if (result.stdout === "") return false;
|
|
2901
|
+
const lines = (result.stdout.endsWith("\n") ? result.stdout.slice(0, -1) : result.stdout).split(
|
|
2902
|
+
"\n"
|
|
2903
|
+
);
|
|
2904
|
+
if (lines.length !== 1) return null;
|
|
2905
|
+
const match = /^(?:[a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/i.exec(lines[0]);
|
|
2906
|
+
return match?.[1] === ref ? true : null;
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
async function abandonedLocalCiReason(record, repoRoot, readers) {
|
|
2912
|
+
if (record.pullRequestNumber !== void 0) {
|
|
2913
|
+
const state = await (readers.readPullRequestState ?? readGitHubPullRequestState)({
|
|
2914
|
+
repository: record.repository,
|
|
2915
|
+
number: record.pullRequestNumber
|
|
2916
|
+
}).catch(() => null);
|
|
2917
|
+
if (state === "CLOSED" || state === "MERGED") {
|
|
2918
|
+
return `Pull request #${record.pullRequestNumber} is ${state.toLowerCase()}; validation recovery cancelled.`;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const exists = await (readers.readRemoteBranchExists ?? readRemoteBranchExists)(
|
|
2922
|
+
repoRoot,
|
|
2923
|
+
record.remote,
|
|
2924
|
+
record.branch
|
|
2925
|
+
).catch(() => null);
|
|
2926
|
+
return exists === false ? `Remote branch refs/heads/${record.branch} no longer exists; validation recovery cancelled.` : void 0;
|
|
2927
|
+
}
|
|
2928
|
+
async function retryRetainedWorktreeCleanup(record, store, removeWorktree, repoRoot) {
|
|
2929
|
+
if (!record.worktreePath) return record;
|
|
2930
|
+
try {
|
|
2931
|
+
await removeWorktree(repoRoot, record.worktreePath);
|
|
2932
|
+
} catch {
|
|
2933
|
+
return record;
|
|
2934
|
+
}
|
|
2935
|
+
const transition = await store.compareAndSave(record, {
|
|
2936
|
+
...record,
|
|
2937
|
+
repoRoot,
|
|
2938
|
+
worktreePath: void 0,
|
|
2939
|
+
// Drop cleanup-only notes once the path is gone; keep real validation errors.
|
|
2940
|
+
error: record.error?.startsWith("Worktree cleanup pending:") ? void 0 : record.error,
|
|
2941
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2942
|
+
});
|
|
2943
|
+
return transition.record;
|
|
2944
|
+
}
|
|
2945
|
+
async function retireAbandonedLocalCiRun(record, repoRoot, store, removeWorktree, readers) {
|
|
2946
|
+
const reason = await abandonedLocalCiReason(record, repoRoot, readers);
|
|
2947
|
+
if (!reason) return void 0;
|
|
2948
|
+
const transition = await store.compareAndSave(record, {
|
|
2949
|
+
...record,
|
|
2950
|
+
repoRoot,
|
|
2951
|
+
status: "superseded",
|
|
2952
|
+
pid: void 0,
|
|
2953
|
+
pidIdentity: void 0,
|
|
2954
|
+
error: reason,
|
|
2955
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2956
|
+
});
|
|
2957
|
+
return transition.applied ? retryRetainedWorktreeCleanup(transition.record, store, removeWorktree, repoRoot) : transition.record;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// src/core/ci/local-ci-coordinator.ts
|
|
2961
|
+
var LOCAL_CI_CONSECUTIVE_FAILURE_HALT = 2;
|
|
2962
|
+
var MAX_AUTOMATIC_RECOVERY_ATTEMPTS = 3;
|
|
2963
|
+
var ShaSchema = z3.string().regex(/^[a-f0-9]{40}$/i);
|
|
2964
|
+
var BranchSchema = z3.string().min(1).refine((value) => !/[\s~^:?*[\]\\]/.test(value) && !value.includes(".."), "Invalid branch name");
|
|
2965
|
+
async function resolveKnownLocalCiRepository(store, repoRoot) {
|
|
2966
|
+
return await store.repositoryForRoot(repoRoot) ?? await resolveOptionalGitHubRepositoryFromRemote(repoRoot, "origin");
|
|
2967
|
+
}
|
|
2968
|
+
function processIsAlive(pid, pidIdentity, ownedAt) {
|
|
2969
|
+
if (!pid) return false;
|
|
2970
|
+
return processOwnershipState(pid, pidIdentity, ownedAt) !== "reclaimable";
|
|
2971
|
+
}
|
|
2972
|
+
function withLauncherOwnership(record) {
|
|
2973
|
+
const pidIdentity = readProcessIdentity(process.pid);
|
|
2974
|
+
return pidIdentity ? { ...record, pid: process.pid, pidIdentity } : record;
|
|
2975
|
+
}
|
|
2976
|
+
function cliEntryPath() {
|
|
2977
|
+
const entry = process.argv[1];
|
|
2978
|
+
if (!entry) throw new Error("Unable to resolve the Build CLI entry path for local CI.");
|
|
2979
|
+
return path4.resolve(entry);
|
|
2980
|
+
}
|
|
2981
|
+
function createLocalCiWorkerChildEnv() {
|
|
2982
|
+
return withApprovedEntrypointEnv(
|
|
2983
|
+
createLocalCiDetachedChildEnv({ BUILD_LOCAL_CI_WORKER: "1", BUILD_CI_RECONCILING: "1" })
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
function prepareRequeuedRecord(record, repoRoot, options = {}) {
|
|
2987
|
+
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2988
|
+
return {
|
|
2989
|
+
...record,
|
|
2990
|
+
// Canonical root always wins over the record's own (possibly-stale) repoRoot:
|
|
2991
|
+
// a requeue relaunches a worker that must spawn git in a surviving working
|
|
2992
|
+
// tree, never a since-deleted submitting worktree.
|
|
2993
|
+
repoRoot,
|
|
2994
|
+
status: "queued",
|
|
2995
|
+
commands: [],
|
|
2996
|
+
pid: void 0,
|
|
2997
|
+
pidIdentity: void 0,
|
|
2998
|
+
worktreePath: void 0,
|
|
2999
|
+
startedAt: void 0,
|
|
3000
|
+
queueWaitMs: void 0,
|
|
3001
|
+
submittedAt: options.refreshSubmissionAt ? timestamp : record.submittedAt ?? record.createdAt,
|
|
3002
|
+
createdAt: timestamp,
|
|
3003
|
+
updatedAt: timestamp,
|
|
3004
|
+
error: options.error,
|
|
3005
|
+
// Always assign so requeues clear the counter unless the caller carries it.
|
|
3006
|
+
recoveryAttempts: options.recoveryAttempts,
|
|
3007
|
+
// A fresh generation owns its own failing-test telemetry.
|
|
3008
|
+
failingTests: void 0,
|
|
3009
|
+
failingTestsTruncated: void 0,
|
|
3010
|
+
failureClass: void 0,
|
|
3011
|
+
gateRetry: void 0,
|
|
3012
|
+
entrypointApprovalPending: void 0,
|
|
3013
|
+
// Explicit same-SHA resubmit and automatic requeue both start a fresh run.
|
|
3014
|
+
outstandingRemoteStatusPublication: void 0,
|
|
3015
|
+
failurePublicationLease: void 0,
|
|
3016
|
+
...options.clearTrustedBase ? {
|
|
3017
|
+
trustedBaseRef: void 0,
|
|
3018
|
+
trustedBaseSha: void 0,
|
|
3019
|
+
pullRequestNumber: void 0
|
|
3020
|
+
} : {}
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
async function casReconcileTransition(store, expected, next) {
|
|
3024
|
+
return store.compareAndSave(expected, next);
|
|
3025
|
+
}
|
|
3026
|
+
async function casRequeueAndLaunch(store, expected, queued, launchWorker) {
|
|
3027
|
+
if (hasLiveFailurePublicationLease(expected)) {
|
|
3028
|
+
return expected;
|
|
3029
|
+
}
|
|
3030
|
+
const transition = await casReconcileTransition(store, expected, queued);
|
|
3031
|
+
if (!transition.applied) return transition.record;
|
|
3032
|
+
return (await launchWorker(transition.record, store)).record;
|
|
3033
|
+
}
|
|
3034
|
+
async function publishPendingThenRequeueAndLaunch(store, expected, repoRoot, options, launchWorker, dependencies) {
|
|
3035
|
+
const publicationPending = {
|
|
3036
|
+
...expected,
|
|
3037
|
+
status: "publication_pending",
|
|
3038
|
+
error: options.error,
|
|
3039
|
+
outstandingRemoteStatusPublication: createOutstandingStatusPublication(expected, {
|
|
3040
|
+
state: "pending",
|
|
3041
|
+
description: "Validated base changed; exact-SHA revalidation pending",
|
|
3042
|
+
targetStatus: "succeeded_pending"
|
|
3043
|
+
}),
|
|
3044
|
+
failurePublicationLease: void 0,
|
|
3045
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3046
|
+
};
|
|
3047
|
+
const transition = await casReconcileTransition(store, expected, publicationPending);
|
|
3048
|
+
if (!transition.applied) return transition.record;
|
|
3049
|
+
const published = await attemptOutstandingStatusPublication(
|
|
3050
|
+
transition.record,
|
|
3051
|
+
store,
|
|
3052
|
+
dependencies
|
|
3053
|
+
);
|
|
3054
|
+
if (published.status !== "succeeded_pending" || hasOutstandingRemoteStatusPublication(published)) {
|
|
3055
|
+
return published;
|
|
3056
|
+
}
|
|
3057
|
+
return casRequeueAndLaunch(
|
|
3058
|
+
store,
|
|
3059
|
+
published,
|
|
3060
|
+
prepareRequeuedRecord(published, repoRoot, options),
|
|
3061
|
+
launchWorker
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
async function attemptOutstandingStatusPublication(record, store, dependencies = {}) {
|
|
3065
|
+
const obligation = record.outstandingRemoteStatusPublication;
|
|
3066
|
+
if (!obligation) return record;
|
|
3067
|
+
if (!outstandingStatusPublicationMatchesRecord(record, obligation)) {
|
|
3068
|
+
return record;
|
|
3069
|
+
}
|
|
3070
|
+
const publishStatus = dependencies.publishStatus ?? publishGitHubLocalCiStatus;
|
|
3071
|
+
const readRemoteStatus = dependencies.readRemoteStatus ?? readGitHubCommitStatus;
|
|
3072
|
+
let current;
|
|
3073
|
+
try {
|
|
3074
|
+
current = await store.readAndRecover(record.repository, record.sha);
|
|
3075
|
+
} catch {
|
|
3076
|
+
return record;
|
|
3077
|
+
}
|
|
3078
|
+
const currentObligation = current.outstandingRemoteStatusPublication;
|
|
3079
|
+
if (!currentObligation) {
|
|
3080
|
+
return current;
|
|
3081
|
+
}
|
|
3082
|
+
if (!outstandingStatusPublicationMatchesRecord(current, currentObligation)) {
|
|
3083
|
+
return current;
|
|
3084
|
+
}
|
|
3085
|
+
if (!outstandingStatusPublicationMacIsValid(currentObligation)) {
|
|
3086
|
+
return current;
|
|
3087
|
+
}
|
|
3088
|
+
const existingLease = current.failurePublicationLease;
|
|
3089
|
+
if (existingLease && hasLiveFailurePublicationLease(current)) {
|
|
3090
|
+
return current;
|
|
3091
|
+
}
|
|
3092
|
+
const { lease, leased } = createFailurePublicationLease(current);
|
|
3093
|
+
const acquired = await store.compareAndSave(current, leased);
|
|
3094
|
+
if (!acquired.applied) {
|
|
3095
|
+
return acquired.record;
|
|
3096
|
+
}
|
|
3097
|
+
current = acquired.record;
|
|
3098
|
+
const liveObligation = current.outstandingRemoteStatusPublication;
|
|
3099
|
+
if (!matchesFailurePublicationLease(current, lease) || !liveObligation) {
|
|
3100
|
+
return current;
|
|
3101
|
+
}
|
|
3102
|
+
if (!outstandingStatusPublicationMatchesRecord(current, liveObligation)) {
|
|
3103
|
+
return clearFailurePublicationLeaseOnly(store, current, lease);
|
|
3104
|
+
}
|
|
3105
|
+
if (!outstandingStatusPublicationMacIsValid(liveObligation)) {
|
|
3106
|
+
return clearFailurePublicationLeaseOnly(store, current, lease);
|
|
3107
|
+
}
|
|
3108
|
+
try {
|
|
3109
|
+
await publishStatus({
|
|
3110
|
+
repository: liveObligation.repository,
|
|
3111
|
+
sha: liveObligation.sha,
|
|
3112
|
+
state: liveObligation.state,
|
|
3113
|
+
context: liveObligation.context,
|
|
3114
|
+
description: liveObligation.description
|
|
3115
|
+
});
|
|
3116
|
+
} catch {
|
|
3117
|
+
return clearFailurePublicationLeaseOnly(store, current, lease);
|
|
3118
|
+
}
|
|
3119
|
+
return store.withExclusiveRecordAccess(current.repository, current.sha, async (access) => {
|
|
3120
|
+
const requireFence = async () => {
|
|
3121
|
+
try {
|
|
3122
|
+
await access.assertOwned();
|
|
3123
|
+
return true;
|
|
3124
|
+
} catch {
|
|
3125
|
+
return false;
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
if (!await requireFence()) {
|
|
3129
|
+
return current;
|
|
3130
|
+
}
|
|
3131
|
+
let onDisk;
|
|
3132
|
+
try {
|
|
3133
|
+
onDisk = await access.read();
|
|
3134
|
+
} catch {
|
|
3135
|
+
return current;
|
|
3136
|
+
}
|
|
3137
|
+
if (!matchesFailurePublicationLease(onDisk, lease)) {
|
|
3138
|
+
return onDisk;
|
|
3139
|
+
}
|
|
3140
|
+
if (!onDisk.outstandingRemoteStatusPublication) {
|
|
3141
|
+
const dropLease = await access.writeIfSameGeneration(onDisk, {
|
|
3142
|
+
...onDisk,
|
|
3143
|
+
failurePublicationLease: void 0
|
|
3144
|
+
});
|
|
3145
|
+
return dropLease.record;
|
|
3146
|
+
}
|
|
3147
|
+
if (!await requireFence()) {
|
|
3148
|
+
return onDisk;
|
|
3149
|
+
}
|
|
3150
|
+
let remoteStatus;
|
|
3151
|
+
try {
|
|
3152
|
+
remoteStatus = await readRemoteStatus({
|
|
3153
|
+
repository: liveObligation.repository,
|
|
3154
|
+
sha: liveObligation.sha,
|
|
3155
|
+
context: liveObligation.context
|
|
3156
|
+
});
|
|
3157
|
+
} catch {
|
|
3158
|
+
if (!await requireFence()) return onDisk;
|
|
3159
|
+
const dropLease = await access.writeIfSameGeneration(onDisk, {
|
|
3160
|
+
...onDisk,
|
|
3161
|
+
failurePublicationLease: void 0
|
|
3162
|
+
});
|
|
3163
|
+
return dropLease.record;
|
|
3164
|
+
}
|
|
3165
|
+
if (remoteStatus !== liveObligation.state) {
|
|
3166
|
+
if (!await requireFence()) return onDisk;
|
|
3167
|
+
const dropLease = await access.writeIfSameGeneration(onDisk, {
|
|
3168
|
+
...onDisk,
|
|
3169
|
+
failurePublicationLease: void 0
|
|
3170
|
+
});
|
|
3171
|
+
return dropLease.record;
|
|
3172
|
+
}
|
|
3173
|
+
if (!await requireFence()) {
|
|
3174
|
+
return onDisk;
|
|
3175
|
+
}
|
|
3176
|
+
const obligation2 = onDisk.outstandingRemoteStatusPublication;
|
|
3177
|
+
const targetStatus = obligation2?.targetStatus;
|
|
3178
|
+
const publishedStatus = targetStatus ?? onDisk.status;
|
|
3179
|
+
const clearPublicationError = targetStatus === "succeeded" || targetStatus === "succeeded_pending";
|
|
3180
|
+
const completed = await access.writeIfSameGeneration(onDisk, {
|
|
3181
|
+
...onDisk,
|
|
3182
|
+
status: publishedStatus,
|
|
3183
|
+
outstandingRemoteStatusPublication: void 0,
|
|
3184
|
+
failurePublicationLease: void 0,
|
|
3185
|
+
...clearPublicationError ? { error: void 0 } : {},
|
|
3186
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3187
|
+
});
|
|
3188
|
+
return completed.record;
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
async function clearFailurePublicationLeaseOnly(store, expected, lease) {
|
|
3192
|
+
try {
|
|
3193
|
+
const current = await store.read(expected.repository, expected.sha);
|
|
3194
|
+
if (!matchesFailurePublicationLease(current, lease)) {
|
|
3195
|
+
return current;
|
|
3196
|
+
}
|
|
3197
|
+
const cleared = await store.compareAndSave(current, {
|
|
3198
|
+
...current,
|
|
3199
|
+
failurePublicationLease: void 0
|
|
3200
|
+
});
|
|
3201
|
+
return cleared.record;
|
|
3202
|
+
} catch {
|
|
3203
|
+
return expected;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function abandonOrphanedWorkerChild(pid) {
|
|
3207
|
+
try {
|
|
3208
|
+
process.kill(pid, "SIGTERM");
|
|
3209
|
+
} catch {
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
async function spawnWorker(record, store) {
|
|
3213
|
+
const authorized = record;
|
|
3214
|
+
const claim = await store.claimWorker(authorized);
|
|
3215
|
+
if (!claim) {
|
|
3216
|
+
try {
|
|
3217
|
+
const current = await store.read(authorized.repository, authorized.sha);
|
|
3218
|
+
return {
|
|
3219
|
+
record: current,
|
|
3220
|
+
spawned: Boolean(processIsAlive(current.pid, current.pidIdentity, current.updatedAt))
|
|
3221
|
+
};
|
|
3222
|
+
} catch {
|
|
3223
|
+
return { record: authorized, spawned: false };
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
let claimOwnerPid = process.pid;
|
|
3227
|
+
const logPath = path4.join(
|
|
3228
|
+
store.repositoryDirectory(authorized.repository),
|
|
3229
|
+
`${authorized.sha}.log`
|
|
3230
|
+
);
|
|
3231
|
+
const logFd = openSync(logPath, "a", 384);
|
|
3232
|
+
let child;
|
|
3233
|
+
let childPidIdentity;
|
|
3234
|
+
try {
|
|
3235
|
+
child = spawn(
|
|
3236
|
+
process.execPath,
|
|
3237
|
+
[
|
|
3238
|
+
cliEntryPath(),
|
|
3239
|
+
"ci",
|
|
3240
|
+
"worker",
|
|
3241
|
+
"--repository",
|
|
3242
|
+
authorized.repository,
|
|
3243
|
+
"--sha",
|
|
3244
|
+
authorized.sha
|
|
3245
|
+
],
|
|
3246
|
+
{
|
|
3247
|
+
cwd: authorized.repoRoot,
|
|
3248
|
+
detached: process.platform !== "win32",
|
|
3249
|
+
stdio: ["ignore", logFd, logFd],
|
|
3250
|
+
env: createLocalCiWorkerChildEnv()
|
|
3251
|
+
}
|
|
3252
|
+
);
|
|
3253
|
+
if (!child.pid) throw new Error("Local CI worker did not receive a process id.");
|
|
3254
|
+
childPidIdentity = readProcessIdentity(child.pid);
|
|
3255
|
+
if (!childPidIdentity) {
|
|
3256
|
+
throw new Error("Local CI worker process identity could not be established.");
|
|
3257
|
+
}
|
|
3258
|
+
await store.transferWorkerClaim(claim, child.pid, childPidIdentity);
|
|
3259
|
+
claimOwnerPid = child.pid;
|
|
3260
|
+
} catch (error) {
|
|
3261
|
+
await store.releaseWorkerClaim(authorized.repository, authorized.sha, claimOwnerPid, claim.generationKey).catch(() => void 0);
|
|
3262
|
+
if (child?.pid) {
|
|
3263
|
+
abandonOrphanedWorkerChild(child.pid);
|
|
3264
|
+
}
|
|
3265
|
+
if (authorized.pid === process.pid) {
|
|
3266
|
+
await store.compareAndSave(authorized, { ...authorized, pid: void 0, pidIdentity: void 0 }).catch(() => void 0);
|
|
3267
|
+
}
|
|
3268
|
+
throw error;
|
|
3269
|
+
} finally {
|
|
3270
|
+
closeSync(logFd);
|
|
3271
|
+
}
|
|
3272
|
+
if (!child?.pid) throw new Error("Local CI worker did not start.");
|
|
3273
|
+
const childPid = child.pid;
|
|
3274
|
+
if (!childPidIdentity) {
|
|
3275
|
+
throw new Error("Local CI worker process identity could not be established.");
|
|
3276
|
+
}
|
|
3277
|
+
const assignment = await store.assignWorkerPid(authorized, childPid, childPidIdentity);
|
|
3278
|
+
if (!assignment.applied) {
|
|
3279
|
+
abandonOrphanedWorkerChild(childPid);
|
|
3280
|
+
await store.releaseWorkerClaim(authorized.repository, authorized.sha, childPid, claim.generationKey).catch(() => void 0);
|
|
3281
|
+
return { record: assignment.record, spawned: false };
|
|
3282
|
+
}
|
|
3283
|
+
child.unref();
|
|
3284
|
+
await registerDetachedWorkerProcess({
|
|
3285
|
+
pid: childPid,
|
|
3286
|
+
taskId: `local-ci-${authorized.sha.slice(0, 12)}`,
|
|
3287
|
+
command: "build ci worker",
|
|
3288
|
+
owner: "local-ci",
|
|
3289
|
+
metadata: { repository: authorized.repository, sha: authorized.sha }
|
|
3290
|
+
});
|
|
3291
|
+
return { record: assignment.record, spawned: true };
|
|
3292
|
+
}
|
|
3293
|
+
async function waitForWorkerAssignment(store, repository, sha) {
|
|
3294
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
3295
|
+
const record = await store.read(repository, sha);
|
|
3296
|
+
if (record.pid === process.pid) return record;
|
|
3297
|
+
if (record.status === "queued" && (record.pid === void 0 || record.pid === process.ppid || !processIsAlive(record.pid, record.pidIdentity, record.updatedAt))) {
|
|
3298
|
+
await delay4(50);
|
|
3299
|
+
continue;
|
|
3300
|
+
}
|
|
3301
|
+
throw new Error(
|
|
3302
|
+
`Local CI worker assignment lost for ${repository}@${sha}: status=${record.status} pid=${record.pid ?? "none"}`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
throw new Error("Local CI worker assignment was not persisted before startup.");
|
|
3306
|
+
}
|
|
3307
|
+
async function submitLocalCi(input, dependencies = {}) {
|
|
3308
|
+
const submissionRoot = path4.resolve(input.repoRoot);
|
|
3309
|
+
const sha = ShaSchema.parse(input.sha).toLowerCase();
|
|
3310
|
+
const branch = BranchSchema.parse(input.branch);
|
|
3311
|
+
const remote = input.remote ?? "origin";
|
|
3312
|
+
let repository;
|
|
3313
|
+
try {
|
|
3314
|
+
repository = await resolveGitHubRepositoryFromRemote(submissionRoot, remote);
|
|
3315
|
+
} catch {
|
|
3316
|
+
return { skipped: true, reason: `remote '${remote}' is not a supported github.com repository` };
|
|
3317
|
+
}
|
|
3318
|
+
let repoRoot;
|
|
3319
|
+
try {
|
|
3320
|
+
repoRoot = await resolveCanonicalMainRepoRoot(submissionRoot);
|
|
3321
|
+
} catch {
|
|
3322
|
+
return {
|
|
3323
|
+
skipped: true,
|
|
3324
|
+
reason: `unable to resolve canonical repository root from ${submissionRoot}`
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
if (repoRoot !== submissionRoot) {
|
|
3328
|
+
let canonicalRepository;
|
|
3329
|
+
try {
|
|
3330
|
+
canonicalRepository = await resolveGitHubRepositoryFromRemote(repoRoot, remote);
|
|
3331
|
+
} catch {
|
|
3332
|
+
return {
|
|
3333
|
+
skipped: true,
|
|
3334
|
+
reason: `canonical repository root ${repoRoot} has no supported github.com remote '${remote}'`
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
if (canonicalRepository !== repository) {
|
|
3338
|
+
return {
|
|
3339
|
+
skipped: true,
|
|
3340
|
+
reason: `canonical repository root ${repoRoot} resolves to ${canonicalRepository}, not ${repository}`
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
const hasCommit = dependencies.hasCommit ?? (async (root, candidateSha) => {
|
|
3345
|
+
const result = await execGit(["cat-file", "-e", `${candidateSha}^{commit}`], { cwd: root });
|
|
3346
|
+
return result.exitCode === 0;
|
|
3347
|
+
});
|
|
3348
|
+
if (!await hasCommit(repoRoot, sha)) {
|
|
3349
|
+
return {
|
|
3350
|
+
skipped: true,
|
|
3351
|
+
reason: `commit ${sha.slice(0, 12)} is not present locally; run: git fetch ${remote} ${branch}`
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
const store = new LocalCiStateStore(input.stateRoot ?? getLocalCiStateRoot());
|
|
3355
|
+
await store.registerRepositoryRoot(repository, repoRoot);
|
|
3356
|
+
const failures = await store.countConsecutiveTerminalFailures(repository, branch);
|
|
3357
|
+
if (failures.count >= LOCAL_CI_CONSECUTIVE_FAILURE_HALT) {
|
|
3358
|
+
if (!await resolveHaltOverride(input.trust)) {
|
|
3359
|
+
const recent = failures.failedShas.slice(0, 2).map((value) => value.slice(0, 12)).join(", ");
|
|
3360
|
+
return {
|
|
3361
|
+
skipped: true,
|
|
3362
|
+
reason: `${failures.count} consecutive Build Local CI failures on ${branch} (last: ${recent}). Halting automatic gate submission - human review required. Rerun with BUILD_LOCAL_CI_OVERRIDE_HALT=1 (build ci merge --override-halt for a merge request) after review`
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
await appendLandingLogEntry(store.root, "halt-override", {
|
|
3366
|
+
repository,
|
|
3367
|
+
sha,
|
|
3368
|
+
branch,
|
|
3369
|
+
consecutiveFailures: failures.count
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
const queued = withLauncherOwnership(
|
|
3373
|
+
createQueuedRun({
|
|
3374
|
+
repoRoot,
|
|
3375
|
+
repository,
|
|
3376
|
+
sha,
|
|
3377
|
+
branch,
|
|
3378
|
+
remote
|
|
3379
|
+
})
|
|
3380
|
+
);
|
|
3381
|
+
const created = await store.create(queued);
|
|
3382
|
+
let record = created.record;
|
|
3383
|
+
const approvedEntrypointResubmit = record.status === "succeeded_pending" && record.entrypointApprovalPending === true && resolveApprovedEntrypointSha(input.trust)?.toLowerCase() === record.sha.toLowerCase();
|
|
3384
|
+
if (!created.created && (record.status === "succeeded" || // Validation fact held pending remote publication — do not start a fresh generation.
|
|
3385
|
+
record.status === "publication_pending" || // succeeded_pending is terminal only when trusted-base policy resolved.
|
|
3386
|
+
// Without trustedBaseSha, authority never loaded — allow same-SHA restart.
|
|
3387
|
+
// Legacy merge-era statuses normalize to succeeded at read time in local-ci-state.
|
|
3388
|
+
record.status === "succeeded_pending" && record.branch === branch && record.trustedBaseSha !== void 0 && !approvedEntrypointResubmit || // A retained path is a durable cleanup obligation. Reconcile must clear it
|
|
3389
|
+
// before any same-SHA replacement validation can establish a fresh generation.
|
|
3390
|
+
record.worktreePath !== void 0 || processIsAlive(record.pid, record.pidIdentity, record.updatedAt) || // Live publication lease serializes remote writes — no fresh generation yet.
|
|
3391
|
+
hasLiveFailurePublicationLease(record))) {
|
|
3392
|
+
return { record, created: false, spawned: false };
|
|
3393
|
+
}
|
|
3394
|
+
if (!created.created) {
|
|
3395
|
+
const existing = record;
|
|
3396
|
+
const unresolvedTrustedBase = existing.status === "succeeded_pending" && existing.trustedBaseSha === void 0;
|
|
3397
|
+
const approvedEntrypointRestart = existing.status === "succeeded_pending" && approvedEntrypointResubmit;
|
|
3398
|
+
const requeued = withLauncherOwnership({
|
|
3399
|
+
...prepareRequeuedRecord(existing, repoRoot, {
|
|
3400
|
+
error: void 0,
|
|
3401
|
+
refreshSubmissionAt: true,
|
|
3402
|
+
// Clear absent trusted-base fields for unresolved-authority recovery only.
|
|
3403
|
+
...unresolvedTrustedBase || approvedEntrypointRestart ? { clearTrustedBase: true } : {}
|
|
3404
|
+
}),
|
|
3405
|
+
branch
|
|
3406
|
+
});
|
|
3407
|
+
const transition = await store.compareAndSave(existing, requeued);
|
|
3408
|
+
if (!transition.applied) {
|
|
3409
|
+
return { record: transition.record, created: false, spawned: false };
|
|
3410
|
+
}
|
|
3411
|
+
record = transition.record;
|
|
3412
|
+
}
|
|
3413
|
+
await store.supersedeOlderQueued({ repository, branch, newerSha: sha });
|
|
3414
|
+
const launchWorker = dependencies.spawnWorker ?? spawnWorker;
|
|
3415
|
+
const spawned = await launchWorker(record, store);
|
|
3416
|
+
return { record: spawned.record, created: created.created, spawned: spawned.spawned };
|
|
3417
|
+
}
|
|
3418
|
+
async function runLocalCiWorker(input) {
|
|
3419
|
+
const store = new LocalCiStateStore(input.stateRoot ?? getLocalCiStateRoot());
|
|
3420
|
+
const sha = ShaSchema.parse(input.sha).toLowerCase();
|
|
3421
|
+
const record = await waitForWorkerAssignment(store, input.repository, sha);
|
|
3422
|
+
return executeClaimedLocalCiRecord({
|
|
3423
|
+
record,
|
|
3424
|
+
store,
|
|
3425
|
+
slot: input.slot,
|
|
3426
|
+
manageProcess: input.manageProcess,
|
|
3427
|
+
trust: input.trust,
|
|
3428
|
+
signal: input.signal,
|
|
3429
|
+
publishOutstandingStatus: (value) => attemptOutstandingStatusPublication(value, store, {
|
|
3430
|
+
publishStatus: publishGitHubLocalCiStatus,
|
|
3431
|
+
readRemoteStatus: readGitHubCommitStatus
|
|
3432
|
+
})
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
async function reconcileSucceededPendingPublication(record, store, launchWorker, dependencies, repoRoot) {
|
|
3436
|
+
const trustedBaseSha = record.trustedBaseSha;
|
|
3437
|
+
const trustedBaseRef = record.trustedBaseRef;
|
|
3438
|
+
if (!trustedBaseSha || !trustedBaseRef) return record;
|
|
3439
|
+
let pullRequest;
|
|
3440
|
+
try {
|
|
3441
|
+
pullRequest = await (dependencies.findPullRequest ?? findGitHubPullRequest)({
|
|
3442
|
+
repository: record.repository,
|
|
3443
|
+
branch: record.branch,
|
|
3444
|
+
number: record.pullRequestNumber,
|
|
3445
|
+
base: trustedBaseRef,
|
|
3446
|
+
attempts: 1
|
|
3447
|
+
});
|
|
3448
|
+
} catch (error) {
|
|
3449
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3450
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3451
|
+
...record,
|
|
3452
|
+
status: "succeeded_pending",
|
|
3453
|
+
error: `Success attestation pending pull request visibility: ${detail}`,
|
|
3454
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3455
|
+
});
|
|
3456
|
+
return transition2.record;
|
|
3457
|
+
}
|
|
3458
|
+
if (!pullRequest || pullRequest.draft) {
|
|
3459
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3460
|
+
...record,
|
|
3461
|
+
status: "succeeded_pending",
|
|
3462
|
+
error: !pullRequest ? "Success attestation pending pull request visibility." : "Success attestation pending non-draft pull request.",
|
|
3463
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3464
|
+
});
|
|
3465
|
+
return transition2.record;
|
|
3466
|
+
}
|
|
3467
|
+
if (pullRequest.headSha.toLowerCase() !== record.sha.toLowerCase()) {
|
|
3468
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3469
|
+
...record,
|
|
3470
|
+
status: "superseded",
|
|
3471
|
+
error: "Pull request head changed after local validation.",
|
|
3472
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3473
|
+
});
|
|
3474
|
+
return transition2.record;
|
|
3475
|
+
}
|
|
3476
|
+
if (!pullRequest.baseSha) {
|
|
3477
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3478
|
+
...record,
|
|
3479
|
+
status: "succeeded_pending",
|
|
3480
|
+
error: "Success attestation pending trusted PR base: PR base SHA is missing.",
|
|
3481
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3482
|
+
});
|
|
3483
|
+
return transition2.record;
|
|
3484
|
+
}
|
|
3485
|
+
if (pullRequest.baseSha.toLowerCase() !== trustedBaseSha.toLowerCase()) {
|
|
3486
|
+
return casRequeueAndLaunch(
|
|
3487
|
+
store,
|
|
3488
|
+
record,
|
|
3489
|
+
prepareRequeuedRecord(record, repoRoot, {
|
|
3490
|
+
error: `Trusted PR base changed: validated ${trustedBaseSha.slice(0, 12)}, observed ${pullRequest.baseSha.slice(0, 12)}; exact-SHA validation requeued by build ci reconcile.`,
|
|
3491
|
+
clearTrustedBase: true
|
|
3492
|
+
}),
|
|
3493
|
+
launchWorker
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
const createWorktree = dependencies.createWorktree ?? createLocalCiWorktree;
|
|
3497
|
+
const removeWorktree = dependencies.removeWorktree ?? removeLocalCiWorktree;
|
|
3498
|
+
let candidateWorktree;
|
|
3499
|
+
let authority;
|
|
3500
|
+
try {
|
|
3501
|
+
candidateWorktree = await createWorktree(repoRoot, record.sha);
|
|
3502
|
+
authority = await (dependencies.resolveAuthority ?? ((value, worktree, root) => resolveLocalCiAuthority(value, worktree, root, {}, dependencies.trust)))(record, candidateWorktree, repoRoot);
|
|
3503
|
+
} catch (error) {
|
|
3504
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3505
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3506
|
+
...record,
|
|
3507
|
+
status: "succeeded_pending",
|
|
3508
|
+
error: `Success attestation pending authority check: ${detail}`,
|
|
3509
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3510
|
+
});
|
|
3511
|
+
return transition2.record;
|
|
3512
|
+
} finally {
|
|
3513
|
+
if (candidateWorktree) {
|
|
3514
|
+
await removeWorktree(repoRoot, candidateWorktree).catch(() => void 0);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
const authorityPatch = {
|
|
3518
|
+
statusContext: authority.statusContext,
|
|
3519
|
+
maxConcurrent: authority.maxConcurrent,
|
|
3520
|
+
trustedBaseRef: authority.trustedBaseRef,
|
|
3521
|
+
trustedBaseSha: authority.trustedBaseSha,
|
|
3522
|
+
pullRequestNumber: authority.pullRequest?.number ?? pullRequest.number ?? record.pullRequestNumber,
|
|
3523
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3524
|
+
};
|
|
3525
|
+
if (!authority.enabled) {
|
|
3526
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3527
|
+
...record,
|
|
3528
|
+
...authorityPatch,
|
|
3529
|
+
status: "skipped",
|
|
3530
|
+
error: "Local CI disabled by trusted base policy."
|
|
3531
|
+
});
|
|
3532
|
+
return transition2.record;
|
|
3533
|
+
}
|
|
3534
|
+
if (!authority.trustedBaseSha || authority.trustedBaseSha.toLowerCase() !== trustedBaseSha.toLowerCase() || authority.trustedBaseSha.toLowerCase() !== pullRequest.baseSha.toLowerCase() || authority.trustedBaseRef !== pullRequest.baseRefName) {
|
|
3535
|
+
return casRequeueAndLaunch(
|
|
3536
|
+
store,
|
|
3537
|
+
record,
|
|
3538
|
+
prepareRequeuedRecord(record, repoRoot, {
|
|
3539
|
+
error: "Trusted-base authority changed; exact-SHA validation requeued.",
|
|
3540
|
+
clearTrustedBase: true
|
|
3541
|
+
}),
|
|
3542
|
+
launchWorker
|
|
3543
|
+
);
|
|
3544
|
+
}
|
|
3545
|
+
if (!authority.trusted) {
|
|
3546
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3547
|
+
...record,
|
|
3548
|
+
...authorityPatch,
|
|
3549
|
+
status: "succeeded_pending",
|
|
3550
|
+
error: "Success attestation pending trusted-base authority."
|
|
3551
|
+
});
|
|
3552
|
+
return transition2.record;
|
|
3553
|
+
}
|
|
3554
|
+
const entrypointBlockedReason = authority.commandEntrypoint.blockedReason;
|
|
3555
|
+
if (entrypointBlockedReason) {
|
|
3556
|
+
const transition2 = await casReconcileTransition(store, record, {
|
|
3557
|
+
...record,
|
|
3558
|
+
...authorityPatch,
|
|
3559
|
+
status: "succeeded_pending",
|
|
3560
|
+
error: `Success attestation pending trusted verification entrypoint: ${entrypointBlockedReason}. Review the change, then rerun with BUILD_LOCAL_CI_APPROVE_ENTRYPOINT_SHA=${record.sha}.`
|
|
3561
|
+
});
|
|
3562
|
+
return transition2.record;
|
|
3563
|
+
}
|
|
3564
|
+
if (record.entrypointApprovalPending) {
|
|
3565
|
+
return casRequeueAndLaunch(
|
|
3566
|
+
store,
|
|
3567
|
+
record,
|
|
3568
|
+
prepareRequeuedRecord(record, repoRoot, {
|
|
3569
|
+
error: "Trusted verification entrypoint approved; exact-SHA validation requeued.",
|
|
3570
|
+
clearTrustedBase: true
|
|
3571
|
+
}),
|
|
3572
|
+
launchWorker
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
3575
|
+
const commandCount = record.commands?.length ?? 0;
|
|
3576
|
+
const description = commandCount > 0 ? `Local validation passed (${commandCount} command${commandCount === 1 ? "" : "s"})` : "Local validation passed";
|
|
3577
|
+
const publicationRecord = {
|
|
3578
|
+
...record,
|
|
3579
|
+
...authorityPatch
|
|
3580
|
+
};
|
|
3581
|
+
const obligation = createOutstandingStatusPublication(publicationRecord, {
|
|
3582
|
+
state: "success",
|
|
3583
|
+
description,
|
|
3584
|
+
targetStatus: "succeeded"
|
|
3585
|
+
});
|
|
3586
|
+
const publicationPending = {
|
|
3587
|
+
...publicationRecord,
|
|
3588
|
+
status: "publication_pending",
|
|
3589
|
+
error: record.error?.startsWith("Worktree cleanup pending:") ? record.error : void 0,
|
|
3590
|
+
outstandingRemoteStatusPublication: obligation
|
|
3591
|
+
};
|
|
3592
|
+
const transition = await casReconcileTransition(store, record, publicationPending);
|
|
3593
|
+
if (!transition.applied) {
|
|
3594
|
+
return transition.record;
|
|
3595
|
+
}
|
|
3596
|
+
return attemptOutstandingStatusPublication(transition.record, store, dependencies);
|
|
3597
|
+
}
|
|
3598
|
+
async function reconcileSucceededBaseBinding(record, pullRequest, store, launchWorker, dependencies, repoRoot) {
|
|
3599
|
+
const trustedBaseRef = record.trustedBaseRef;
|
|
3600
|
+
if (!trustedBaseRef) return record;
|
|
3601
|
+
if (pullRequest.headSha.toLowerCase() !== record.sha.toLowerCase()) return record;
|
|
3602
|
+
if (pullRequest.baseRefName === trustedBaseRef) return record;
|
|
3603
|
+
return publishPendingThenRequeueAndLaunch(
|
|
3604
|
+
store,
|
|
3605
|
+
record,
|
|
3606
|
+
repoRoot,
|
|
3607
|
+
{
|
|
3608
|
+
error: `Pull request base retargeted from '${trustedBaseRef}' to '${pullRequest.baseRefName}' after success attestation; exact-SHA revalidation requeued.`,
|
|
3609
|
+
clearTrustedBase: true
|
|
3610
|
+
},
|
|
3611
|
+
launchWorker,
|
|
3612
|
+
dependencies
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
async function failAutomaticRecovery(record, store, dependencies, error, clearWorktree = false) {
|
|
3616
|
+
const transition = await casReconcileTransition(store, record, {
|
|
3617
|
+
...record,
|
|
3618
|
+
status: "failed",
|
|
3619
|
+
error,
|
|
3620
|
+
pid: void 0,
|
|
3621
|
+
pidIdentity: void 0,
|
|
3622
|
+
worktreePath: clearWorktree ? void 0 : record.worktreePath,
|
|
3623
|
+
outstandingRemoteStatusPublication: createOutstandingFailureStatusPublication(record, error),
|
|
3624
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3625
|
+
});
|
|
3626
|
+
return transition.applied ? attemptOutstandingStatusPublication(transition.record, store, dependencies) : transition.record;
|
|
3627
|
+
}
|
|
3628
|
+
async function reconcileSucceeded(record, store, launchWorker, dependencies, repoRoot, pullRequest) {
|
|
3629
|
+
if (!record.trustedBaseSha) {
|
|
3630
|
+
const attempts = record.recoveryAttempts ?? 0;
|
|
3631
|
+
if (attempts >= MAX_AUTOMATIC_RECOVERY_ATTEMPTS) {
|
|
3632
|
+
return failAutomaticRecovery(
|
|
3633
|
+
record,
|
|
3634
|
+
store,
|
|
3635
|
+
dependencies,
|
|
3636
|
+
`Trusted-base authority unresolved after ${MAX_AUTOMATIC_RECOVERY_ATTEMPTS} automatic recovery attempts; resubmit the SHA to retry.`
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
const requeueOptions = {
|
|
3640
|
+
error: "Trusted-base validation requeued.",
|
|
3641
|
+
clearTrustedBase: true,
|
|
3642
|
+
recoveryAttempts: attempts + 1
|
|
3643
|
+
};
|
|
3644
|
+
return record.status === "succeeded" ? publishPendingThenRequeueAndLaunch(
|
|
3645
|
+
store,
|
|
3646
|
+
record,
|
|
3647
|
+
repoRoot,
|
|
3648
|
+
requeueOptions,
|
|
3649
|
+
launchWorker,
|
|
3650
|
+
dependencies
|
|
3651
|
+
) : casRequeueAndLaunch(
|
|
3652
|
+
store,
|
|
3653
|
+
record,
|
|
3654
|
+
prepareRequeuedRecord(record, repoRoot, requeueOptions),
|
|
3655
|
+
launchWorker
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
if (record.status === "succeeded_pending") {
|
|
3659
|
+
return reconcileSucceededPendingPublication(
|
|
3660
|
+
record,
|
|
3661
|
+
store,
|
|
3662
|
+
launchWorker,
|
|
3663
|
+
dependencies,
|
|
3664
|
+
repoRoot
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
if (!pullRequest) return record;
|
|
3668
|
+
return reconcileSucceededBaseBinding(
|
|
3669
|
+
record,
|
|
3670
|
+
pullRequest,
|
|
3671
|
+
store,
|
|
3672
|
+
launchWorker,
|
|
3673
|
+
dependencies,
|
|
3674
|
+
repoRoot
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
async function reconcileLocalCiCandidate(input) {
|
|
3678
|
+
const store = new LocalCiStateStore(input.stateRoot ?? getLocalCiStateRoot());
|
|
3679
|
+
const repoRoot = await resolveCanonicalMainRepoRoot(input.repoRoot);
|
|
3680
|
+
let record = await store.read(input.repository, ShaSchema.parse(input.sha).toLowerCase());
|
|
3681
|
+
if (hasLiveFailurePublicationLease(record) || (record.pid !== process.pid || record.status === "queued" || record.status === "running") && processIsAlive(record.pid, record.pidIdentity, record.updatedAt)) {
|
|
3682
|
+
return record;
|
|
3683
|
+
}
|
|
3684
|
+
record = await retryRetainedWorktreeCleanup(record, store, removeLocalCiWorktree, repoRoot);
|
|
3685
|
+
if (record.worktreePath) return record;
|
|
3686
|
+
if (hasOutstandingRemoteStatusPublication(record)) {
|
|
3687
|
+
record = await attemptOutstandingStatusPublication(record, store);
|
|
3688
|
+
}
|
|
3689
|
+
if (record.status === "succeeded" && input.expectedBaseSha && record.trustedBaseSha !== input.expectedBaseSha) {
|
|
3690
|
+
const expectedBaseSha = ShaSchema.parse(input.expectedBaseSha).toLowerCase();
|
|
3691
|
+
const pr = await findGitHubPullRequest({
|
|
3692
|
+
repository: record.repository,
|
|
3693
|
+
branch: record.branch,
|
|
3694
|
+
number: record.pullRequestNumber,
|
|
3695
|
+
attempts: 1
|
|
3696
|
+
});
|
|
3697
|
+
if (!pr || pr.headSha !== record.sha || pr.baseSha !== expectedBaseSha || pr.baseRefName !== record.trustedBaseRef) {
|
|
3698
|
+
return record;
|
|
3699
|
+
}
|
|
3700
|
+
return publishPendingThenRequeueAndLaunch(
|
|
3701
|
+
store,
|
|
3702
|
+
record,
|
|
3703
|
+
repoRoot,
|
|
3704
|
+
{ clearTrustedBase: true, error: "Explicit merge request requires current-base validation." },
|
|
3705
|
+
async (queued) => ({ record: queued, spawned: false }),
|
|
3706
|
+
{}
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
if (record.status !== "succeeded" && record.status !== "succeeded_pending") return record;
|
|
3710
|
+
const pullRequest = record.status === "succeeded" && record.trustedBaseSha && record.trustedBaseRef && record.pullRequestNumber ? await findGitHubPullRequest({
|
|
3711
|
+
repository: record.repository,
|
|
3712
|
+
branch: record.branch,
|
|
3713
|
+
number: record.pullRequestNumber,
|
|
3714
|
+
attempts: 1
|
|
3715
|
+
}).catch(() => null) : null;
|
|
3716
|
+
return reconcileSucceeded(
|
|
3717
|
+
record,
|
|
3718
|
+
store,
|
|
3719
|
+
async (queued) => ({ record: queued, spawned: false }),
|
|
3720
|
+
{ trust: input.trust },
|
|
3721
|
+
repoRoot,
|
|
3722
|
+
pullRequest
|
|
3723
|
+
);
|
|
3724
|
+
}
|
|
3725
|
+
async function reconcileLocalCi(input, dependencies = {}) {
|
|
3726
|
+
const store = new LocalCiStateStore(
|
|
3727
|
+
input.stateRoot ?? getLocalCiStateRoot(),
|
|
3728
|
+
dependencies.storeHooks
|
|
3729
|
+
);
|
|
3730
|
+
const repoRoot = await resolveCanonicalMainRepoRoot(path4.resolve(input.repoRoot));
|
|
3731
|
+
const repository = await resolveKnownLocalCiRepository(store, repoRoot);
|
|
3732
|
+
if (!repository) return [];
|
|
3733
|
+
const records = await store.list(repository);
|
|
3734
|
+
const baseBindingRecheck = await selectBaseBindingRechecks(records, repository, {
|
|
3735
|
+
listOpenPullRequests: dependencies.listOpenPullRequests ?? listGitHubOpenPullRequests,
|
|
3736
|
+
findPullRequest: dependencies.findPullRequest ?? findGitHubPullRequest
|
|
3737
|
+
});
|
|
3738
|
+
const reconciled = [];
|
|
3739
|
+
const isAlive = dependencies.processIsAlive ?? processIsAlive;
|
|
3740
|
+
const launchWorker = dependencies.spawnWorker ?? spawnWorker;
|
|
3741
|
+
const removeWorktree = dependencies.removeWorktree ?? removeLocalCiWorktree;
|
|
3742
|
+
for (const current of records) {
|
|
3743
|
+
let record = current;
|
|
3744
|
+
const ownerInactive = !isAlive(record.pid, record.pidIdentity, record.updatedAt);
|
|
3745
|
+
if ((record.status === "queued" || record.status === "running") && ownerInactive) {
|
|
3746
|
+
const retired = await retireAbandonedLocalCiRun(
|
|
3747
|
+
record,
|
|
3748
|
+
repoRoot,
|
|
3749
|
+
store,
|
|
3750
|
+
removeWorktree,
|
|
3751
|
+
dependencies
|
|
3752
|
+
);
|
|
3753
|
+
if (retired) {
|
|
3754
|
+
reconciled.push(retired);
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
let retainedWorktreePath = record.worktreePath;
|
|
3758
|
+
if (retainedWorktreePath) {
|
|
3759
|
+
try {
|
|
3760
|
+
await removeWorktree(repoRoot, retainedWorktreePath);
|
|
3761
|
+
retainedWorktreePath = void 0;
|
|
3762
|
+
} catch {
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
const superseded = await store.hasNewerSubmission({
|
|
3766
|
+
repository: record.repository,
|
|
3767
|
+
branch: record.branch,
|
|
3768
|
+
sha: record.sha,
|
|
3769
|
+
createdAt: record.createdAt,
|
|
3770
|
+
submittedAt: record.submittedAt
|
|
3771
|
+
});
|
|
3772
|
+
if (superseded) {
|
|
3773
|
+
const transition = await casReconcileTransition(store, record, {
|
|
3774
|
+
...record,
|
|
3775
|
+
repoRoot,
|
|
3776
|
+
status: "superseded",
|
|
3777
|
+
pid: void 0,
|
|
3778
|
+
pidIdentity: void 0,
|
|
3779
|
+
worktreePath: retainedWorktreePath,
|
|
3780
|
+
error: "Superseded by a newer submission for this branch.",
|
|
3781
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3782
|
+
});
|
|
3783
|
+
record = transition.record;
|
|
3784
|
+
} else if (retainedWorktreePath) {
|
|
3785
|
+
const transition = await casReconcileTransition(store, record, {
|
|
3786
|
+
...record,
|
|
3787
|
+
repoRoot,
|
|
3788
|
+
status: "queued",
|
|
3789
|
+
pid: void 0,
|
|
3790
|
+
pidIdentity: void 0,
|
|
3791
|
+
worktreePath: retainedWorktreePath,
|
|
3792
|
+
error: "Worktree cleanup pending: replacement validation not started.",
|
|
3793
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3794
|
+
});
|
|
3795
|
+
record = transition.record;
|
|
3796
|
+
} else if ((record.recoveryAttempts ?? 0) >= MAX_AUTOMATIC_RECOVERY_ATTEMPTS) {
|
|
3797
|
+
record = await failAutomaticRecovery(
|
|
3798
|
+
record,
|
|
3799
|
+
store,
|
|
3800
|
+
dependencies,
|
|
3801
|
+
`Local CI worker exited after ${MAX_AUTOMATIC_RECOVERY_ATTEMPTS} automatic recovery attempts; resubmit the SHA to retry.`,
|
|
3802
|
+
true
|
|
3803
|
+
);
|
|
3804
|
+
} else {
|
|
3805
|
+
record = await casRequeueAndLaunch(
|
|
3806
|
+
store,
|
|
3807
|
+
record,
|
|
3808
|
+
prepareRequeuedRecord(record, repoRoot, {
|
|
3809
|
+
error: void 0,
|
|
3810
|
+
recoveryAttempts: (record.recoveryAttempts ?? 0) + 1
|
|
3811
|
+
}),
|
|
3812
|
+
launchWorker
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
} else if (isPublicationPending(record) && ownerInactive) {
|
|
3816
|
+
record = await retryRetainedWorktreeCleanup(record, store, removeWorktree, repoRoot);
|
|
3817
|
+
if (hasOutstandingRemoteStatusPublication(record)) {
|
|
3818
|
+
record = await attemptOutstandingStatusPublication(record, store, dependencies);
|
|
3819
|
+
}
|
|
3820
|
+
if ((record.status === "succeeded" || record.status === "succeeded_pending") && !isAlive(record.pid, record.pidIdentity, record.updatedAt)) {
|
|
3821
|
+
record = await reconcileSucceeded(record, store, launchWorker, dependencies, repoRoot);
|
|
3822
|
+
}
|
|
3823
|
+
} else if ((record.status === "succeeded" || record.status === "succeeded_pending") && ownerInactive) {
|
|
3824
|
+
if (record.worktreePath) {
|
|
3825
|
+
record = await retryRetainedWorktreeCleanup(record, store, removeWorktree, repoRoot);
|
|
3826
|
+
}
|
|
3827
|
+
record = await reconcileSucceeded(
|
|
3828
|
+
record,
|
|
3829
|
+
store,
|
|
3830
|
+
launchWorker,
|
|
3831
|
+
dependencies,
|
|
3832
|
+
repoRoot,
|
|
3833
|
+
baseBindingRecheck.get(record.sha)
|
|
3834
|
+
);
|
|
3835
|
+
} else if (record.status === "failed" && hasOutstandingRemoteStatusPublication(record) && ownerInactive) {
|
|
3836
|
+
if (record.worktreePath) {
|
|
3837
|
+
record = await retryRetainedWorktreeCleanup(record, store, removeWorktree, repoRoot);
|
|
3838
|
+
}
|
|
3839
|
+
record = await attemptOutstandingStatusPublication(record, store, dependencies);
|
|
3840
|
+
} else if (record.worktreePath && ownerInactive && record.status !== "queued" && record.status !== "running") {
|
|
3841
|
+
record = await retryRetainedWorktreeCleanup(record, store, removeWorktree, repoRoot);
|
|
3842
|
+
}
|
|
3843
|
+
reconciled.push(record);
|
|
3844
|
+
}
|
|
3845
|
+
try {
|
|
3846
|
+
const selection = await store.selectTerminalRetention(repository, {
|
|
3847
|
+
maxTerminalRecords: DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS
|
|
3848
|
+
});
|
|
3849
|
+
await store.deleteRetentionCandidates(selection.deletable);
|
|
3850
|
+
} catch {
|
|
3851
|
+
}
|
|
3852
|
+
pruneEmptyPublicationJournalDirs();
|
|
3853
|
+
return reconciled;
|
|
3854
|
+
}
|
|
3855
|
+
async function listLocalCiRuns(input) {
|
|
3856
|
+
const store = new LocalCiStateStore(input.stateRoot ?? getLocalCiStateRoot());
|
|
3857
|
+
const repoRoot = await resolveCanonicalMainRepoRoot(path4.resolve(input.repoRoot));
|
|
3858
|
+
const repository = await resolveKnownLocalCiRepository(store, repoRoot);
|
|
3859
|
+
if (!repository) return [];
|
|
3860
|
+
return store.list(repository);
|
|
3861
|
+
}
|
|
3862
|
+
async function currentBranch(repoRoot) {
|
|
3863
|
+
const result = await execGit(["branch", "--show-current"], { cwd: repoRoot });
|
|
3864
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
3865
|
+
throw new Error("Local CI requires a named branch, not a detached HEAD.");
|
|
3866
|
+
}
|
|
3867
|
+
return BranchSchema.parse(result.stdout.trim());
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
export {
|
|
3871
|
+
resolveGitHubBranchSha,
|
|
3872
|
+
readGitHubCommitStatusDetail,
|
|
3873
|
+
appendLandingLogEntry,
|
|
3874
|
+
queueLandingFields,
|
|
3875
|
+
acquireLocalCiSlot,
|
|
3876
|
+
resolveCanonicalMainRepoRoot,
|
|
3877
|
+
keepMacAwakeForCurrentProcess,
|
|
3878
|
+
resolveEntrypointFingerprint,
|
|
3879
|
+
submitLocalCi,
|
|
3880
|
+
runLocalCiWorker,
|
|
3881
|
+
reconcileLocalCiCandidate,
|
|
3882
|
+
reconcileLocalCi,
|
|
3883
|
+
listLocalCiRuns,
|
|
3884
|
+
currentBranch,
|
|
3885
|
+
EntrypointApprovalRequired,
|
|
3886
|
+
dispositionPatch,
|
|
3887
|
+
releaseHold,
|
|
3888
|
+
releaseIfApproved,
|
|
3889
|
+
approvalCommand,
|
|
3890
|
+
resumeApprovedHolds
|
|
3891
|
+
};
|
|
3892
|
+
//# sourceMappingURL=chunk-G3VB2UCZ.js.map
|