@reddoorla/maintenance 0.95.0 → 0.96.0
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/README.md +37 -2
- package/dist/airtable-SJV4ZSMS.js +9 -0
- package/dist/{announce-PGT4SZZB.js → announce-45HVP4J4.js} +7 -7
- package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
- package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
- package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
- package/dist/chunk-3EA43S3R.js.map +1 -0
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
- package/dist/chunk-4UTJ4G6P.js +52 -0
- package/dist/chunk-4UTJ4G6P.js.map +1 -0
- package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
- package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
- package/dist/{chunk-4AIYWWKV.js → chunk-62YWOFVN.js} +36 -5
- package/dist/chunk-62YWOFVN.js.map +1 -0
- package/dist/{chunk-22GKJO5F.js → chunk-6INOBNRO.js} +2 -2
- package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
- package/dist/chunk-72FJZ6LO.js.map +1 -0
- package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
- package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
- package/dist/{chunk-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
- package/dist/chunk-CGQZFQAY.js +38 -0
- package/dist/chunk-CGQZFQAY.js.map +1 -0
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
- package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
- package/dist/chunk-DVJNYPFR.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
- package/dist/chunk-EASYCD6F.js.map +1 -0
- package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
- package/dist/{chunk-DMPHP7UP.js → chunk-EQQ4H6L3.js} +2 -2
- package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
- package/dist/chunk-GEUC5VGM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
- package/dist/chunk-IHHAYXKY.js +351 -0
- package/dist/chunk-IHHAYXKY.js.map +1 -0
- package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
- package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
- package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
- package/dist/chunk-MZZDMBDT.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
- package/dist/chunk-NVLREB5R.js.map +1 -0
- package/dist/{chunk-AEUABPAJ.js → chunk-OTEVKRAH.js} +3 -3
- package/dist/{chunk-KQB2BX4V.js → chunk-P4A6D6HO.js} +4 -4
- package/dist/chunk-P7323YVW.js +134 -0
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
- package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
- package/dist/chunk-SHGIDTYF.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
- package/dist/{chunk-OCNSIBMP.js → chunk-T76MA2FH.js} +135 -1
- package/dist/{chunk-OCNSIBMP.js.map → chunk-T76MA2FH.js.map} +1 -1
- package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
- package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
- package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
- package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
- package/dist/{chunk-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
- package/dist/chunk-XED2DNAI.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
- package/dist/chunk-XG2A6GIG.js.map +1 -0
- package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
- package/dist/chunk-XY7YV2YQ.js.map +1 -0
- package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
- package/dist/chunk-Y6FCXRIC.js +3972 -0
- package/dist/chunk-Y6FCXRIC.js.map +1 -0
- package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
- package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
- package/dist/cli/bin.js +49 -31
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +18 -18
- package/dist/client-L7NAUG6J.js +11 -0
- package/dist/configs/eslint.js +27 -1
- package/dist/configs/eslint.js.map +1 -1
- package/dist/configs/playwright-a11y.d.ts +11 -1
- package/dist/configs/playwright-a11y.js +3 -1
- package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
- package/dist/{db-BBEM7CBX.js → db-5K4CDEGJ.js} +45 -20
- package/dist/db-5K4CDEGJ.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-65BOZ7AT.js → digest-5AJGWS33.js} +45 -22
- package/dist/digest-5AJGWS33.js.map +1 -0
- package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-MC5NWEUA.js} +12 -10
- package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
- package/dist/digest-state-ZDPKR7MW.js.map +1 -0
- package/dist/{ensure-site-WIDO6LGU.js → ensure-site-XESZHZEB.js} +39 -11
- package/dist/ensure-site-XESZHZEB.js.map +1 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
- package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-4NPLX2UG.js} +5 -5
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-GAHNOAHS.js → github-signals-DMU74SAX.js} +10 -10
- package/dist/{header-image-YN3AQRY3.js → header-image-BRTOLHJF.js} +6 -5
- package/dist/header-image-BRTOLHJF.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
- package/dist/{health-mirror-EB76EERV.js → health-mirror-ONRT4DQ5.js} +7 -7
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
- package/dist/index.d.ts +135 -4
- package/dist/index.js +68 -34
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
- package/dist/{launch-4RVWPZGI.js → launch-XTDMC6LD.js} +209 -61
- package/dist/launch-XTDMC6LD.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
- package/dist/{match-harness-2Q6ISEES.js → match-harness-OW45CB5H.js} +14 -7
- package/dist/match-harness-OW45CB5H.js.map +1 -0
- package/dist/migrate-NE3DUMTM.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
- package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
- package/dist/{orchestrate-MWXENQOS.js → orchestrate-TGDFGY52.js} +8 -8
- package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-4VQP7JJL.js → preflight-POZZI3GN.js} +9 -9
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
- package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
- package/dist/{prismic-models-W6CNLTWH.js → prismic-models-URYGODNY.js} +9 -9
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-X4Z67LWW.js → prospect-audit-AJEE3Z4L.js} +16 -7
- package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
- package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-IZVQY3OQ.js} +12 -4
- package/dist/protection-audit-3J3UEOVT.js +413 -0
- package/dist/protection-audit-3J3UEOVT.js.map +1 -0
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
- package/dist/render-MHUUHPLN.js.map +1 -0
- package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-4GFD4MW2.js → report-RC5LGPU3.js} +22 -20
- package/dist/report-RC5LGPU3.js.map +1 -0
- package/dist/{report-mirror-IP64R7B3.js → report-mirror-KKZZ6RMZ.js} +13 -9
- package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
- package/dist/{schema-2V7BGYVT.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
- package/dist/{selftest-MAB2PBXQ.js → selftest-AFLR7YV7.js} +10 -10
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-UKHREAD6.js → site-mirror-H2FYMN55.js} +23 -9
- package/dist/site-mirror-H2FYMN55.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
- package/dist/{submissions-5GKJPCIH.js → submissions-II5J35QM.js} +4 -4
- package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
- package/dist/submissions-JDZP35JM.js.map +1 -0
- package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
- package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
- package/dist/util/git.d.ts +48 -9
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
- package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/chunk-4AIYWWKV.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6VKYQATC.js +0 -1708
- package/dist/chunk-6VKYQATC.js.map +0 -1
- package/dist/chunk-7MKT4I5T.js +0 -62
- package/dist/chunk-7MKT4I5T.js.map +0 -1
- package/dist/chunk-AVEQUR44.js.map +0 -1
- package/dist/chunk-BVEDKMNG.js.map +0 -1
- package/dist/chunk-BYGPUUCT.js.map +0 -1
- package/dist/chunk-CWXTNPNZ.js.map +0 -1
- package/dist/chunk-DFKBTBO3.js.map +0 -1
- package/dist/chunk-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-TO7DJYLZ.js.map +0 -1
- package/dist/chunk-TR3HFH6J.js.map +0 -1
- package/dist/chunk-WJMPCVOR.js.map +0 -1
- package/dist/chunk-WSJ7RO54.js.map +0 -1
- package/dist/chunk-WVNDEYXL.js.map +0 -1
- package/dist/chunk-XHHMM5HX.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/client-M2V6FHH5.js +0 -11
- package/dist/db-BBEM7CBX.js.map +0 -1
- package/dist/digest-65BOZ7AT.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-WIDO6LGU.js.map +0 -1
- package/dist/header-image-YN3AQRY3.js.map +0 -1
- package/dist/launch-4RVWPZGI.js.map +0 -1
- package/dist/match-harness-2Q6ISEES.js.map +0 -1
- package/dist/migrate-K4JETR36.js +0 -7
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prospect-audit-X4Z67LWW.js.map +0 -1
- package/dist/protection-audit-SJO53MEQ.js +0 -168
- package/dist/protection-audit-SJO53MEQ.js.map +0 -1
- package/dist/render-YGNGTX3W.js.map +0 -1
- package/dist/replay-PCSQOGCZ.js.map +0 -1
- package/dist/report-4GFD4MW2.js.map +0 -1
- package/dist/schema-2V7BGYVT.js.map +0 -1
- package/dist/site-mirror-UKHREAD6.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
- /package/dist/{announce-PGT4SZZB.js.map → announce-45HVP4J4.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
- /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
- /package/dist/{chunk-22GKJO5F.js.map → chunk-6INOBNRO.js.map} +0 -0
- /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
- /package/dist/{chunk-DMPHP7UP.js.map → chunk-EQQ4H6L3.js.map} +0 -0
- /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
- /package/dist/{chunk-AEUABPAJ.js.map → chunk-OTEVKRAH.js.map} +0 -0
- /package/dist/{chunk-KQB2BX4V.js.map → chunk-P4A6D6HO.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
- /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
- /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
- /package/dist/{client-M2V6FHH5.js.map → client-L7NAUG6J.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
- /package/dist/{digest-collectors-EHFY2OQC.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
- /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-DMU74SAX.js.map} +0 -0
- /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{migrate-K4JETR36.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
- /package/dist/{orchestrate-MWXENQOS.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
- /package/dist/{prospect-audits-RVYEA4KH.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{preflight-4VQP7JJL.js.map → preflight-POZZI3GN.js.map} +0 -0
- /package/dist/{prismic-models-W6CNLTWH.js.map → prismic-models-URYGODNY.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
- /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
- /package/dist/{selftest-MAB2PBXQ.js.map → selftest-AFLR7YV7.js.map} +0 -0
- /package/dist/{submissions-5GKJPCIH.js.map → submissions-II5J35QM.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/launch.ts","../src/recipes/launch.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { launch, type LaunchResult, type LaunchStepResult } from \"../../recipes/launch.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\n\nexport type LaunchCommandOptions = {\n cwd?: string;\n};\n\nexport function formatStep(name: string, r: LaunchStepResult): string {\n if (r.kind === \"error\") return `${name.padEnd(20)} error: ${r.message}`;\n if (r.kind === \"audit\") {\n const s = r.scores;\n return `${name.padEnd(20)} audited (P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo})`;\n }\n if (r.kind === \"draft\") {\n return `${name.padEnd(20)} drafted ${r.report.reportId}`;\n }\n if (r.kind === \"probe\") return `${name.padEnd(20)} ok — ${r.message}`;\n const rec = r.result;\n if (rec.status === \"noop\") return `${name.padEnd(20)} noop${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n if (rec.status === \"failed\")\n return `${name.padEnd(20)} failed${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n return `${name.padEnd(20)} applied (${rec.commits.length} commit${rec.commits.length === 1 ? \"\" : \"s\"})${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n}\n\nfunction formatResult(r: LaunchResult): string {\n const header = `[${r.site}] launch — ${r.complete ? \"drafted (awaiting approval)\" : \"STOPPED\"}`;\n const body = r.steps.map((s) => formatStep(s.name, s.result)).join(\"\\n\");\n return `${header}\\n${body}`;\n}\n\n/**\n * `launch <site>` — single-site only. Bootstrap (Renovate + protection), first-audit\n * the site, and DRAFT its launch email into the M3 approve queue. Never sends;\n * the operator approves the draft and the next send run delivers the go-live\n * email (flipping Status → maintenance + stamping Launched at).\n */\nexport async function runLaunchCommand(\n site: string,\n opts: LaunchCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n const sites = await resolveSites({ site, cwd });\n const target = sites[0];\n if (!target) {\n return { output: `No site resolved for \"${site}\".`, code: 1 };\n }\n\n // #539 Phase 5: create-side Turso dual-write, wired at the composition root\n // (see runAnnounceCommand for why it is not defaulted inside the recipe).\n const { makeReportMirror } = await import(\"../../reports/report-mirror.js\");\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await launch(target, {\n reportMirror: await makeReportMirror(),\n siteMirror: await makeSiteMirror(),\n });\n return { output: formatResult(result), code: result.complete ? 0 : 1 };\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { selfUpdating } from \"./self-updating/index.js\";\nimport { runAudits } from \"../audits/index.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"../audits/lighthouse-airtable.js\";\nimport { writeAuditsToAirtable } from \"../audits/write-audits-to-airtable.js\";\nimport { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug } from \"../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport {\n createDraft,\n findReportByPeriod,\n updateReportScores,\n} from \"../reports/airtable/reports.js\";\nimport type { ReportRow } from \"../reports/airtable/reports.js\";\nimport type { ReportMirror } from \"../reports/report-mirror.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport { queueDraft } from \"../reports/queue.js\";\nimport { uploadAttachment } from \"../reports/airtable/attachments.js\";\nimport { renderReportHtml } from \"../reports/render.js\";\nimport { resolveCopy } from \"../reports/copy.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\n\nexport type LaunchStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[]; scores: LighthouseScores }\n | { kind: \"draft\"; report: ReportRow }\n | { kind: \"probe\"; message: string }\n | { kind: \"error\"; message: string };\n\nexport type LaunchResult = {\n site: string;\n steps: Array<{ name: string; result: LaunchStepResult }>;\n /** True if every step ran (bootstrap + audit + draft); false if a step\n * errored or a recipe `failed` short-circuited the chain. */\n complete: boolean;\n};\n\nexport type LaunchDeps = {\n /** Bootstrap step (Renovate + protection; ci.yml comes from the starter). Defaults to the real `selfUpdating`. */\n bootstrap?: (site: Site) => Promise<RecipeResult>;\n /** Audit step. Defaults to the real `runAudits`. */\n audit?: (site: Site) => Promise<AuditResult[]>;\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** #539 Phase 5: Turso write-through for everything this recipe writes — the\n * Launch row (or a re-run's refreshed scores), the rendered body, and the\n * queue flag. Wired at the CLI composition root, never defaulted here — the\n * unit suite calls `launch` with a fake base and must not open a real\n * libSQL handle. */\n reportMirror?: ReportMirror;\n /** #539 Phase 5: the Websites-row twin — launch writes the site's FIRST audit\n * results and (on send) its launched status. Injected at the CLI root. */\n siteMirror?: SiteMirror;\n /** HTTP probe for `dev-guard`. Defaults to global fetch. INJECTED in tests:\n * the suite stubs `global.fetch` for the Airtable attachment upload and that\n * stub answers 200 to everything — which is precisely the state this step\n * exists to fail on. */\n probe?: (url: string) => Promise<{ status: number; body: string }>;\n};\n\n/** The site's OWN 404. `src/routes/+error.svelte` renders `<h1>{page.status}</h1>`,\n * so a real SvelteKit 404 from this app carries an `<h1>404</h1>`; a parked\n * domain, a CDN 404 and a dead host do not. \"The route is gone\" and \"the site\n * is gone\" must not look the same to this gate. */\nconst SITE_404_MARKER = /<h1[^>]*>\\s*404\\s*<\\/h1>/i;\n\n/** The UNGUARDED twin's OWN refusal. `src/routes/dev/match/[uid]/+page.server.ts`\n * does `error(404, { message: \\`no matching assembly for \"${uid}\" (have: ...)\\` })`\n * for any uid absent from its assembly map — and that message renders through\n * the very same `+error.svelte` the guard's 404 uses, so it carries an\n * `<h1>404</h1>` too. On a site whose uid set lacks \"home\", a LIVE twin and a\n * guarded one are byte-indistinguishable to SITE_404_MARKER, and the check\n * could never fail. Verified on disk: beachfront-dentistry's copy of that route\n * imports `$app/environment` zero times.\n *\n * BOTH wordings are covered. The on-disk twin says \"no matching assembly for\";\n * the `ROUTE_PAGE` template the `match-harness` recipe will install says \"no\n * assembly for\". A marker that knew only the first would go blind the moment\n * that recipe ships.\n *\n * This is a DENY clause and only ever a deny — it can refuse a green, never\n * grant one. Widening it can only ever refuse a launch that would otherwise\n * have proceeded, so widening is always the safe direction here. */\nconst UNGUARDED_TWIN_MARKER = /no (matching )?assembly for/i;\n\nconst MATCH_ROUTE_DIR = \"src/routes/dev/match\";\n\n/** Positive evidence that `sitePath` is a SvelteKit checkout at all. Without it,\n * the absence of MATCH_ROUTE_DIR is evidence of nothing: a path never cloned, a\n * stale clone, a mistyped site name and the wrong working directory all produce\n * exactly the same `false`, and `resolveSites` builds the path with\n * `localPath(resolve(cwd, site))` and no existence check of its own. This is\n * the filesystem twin of the `/health` control on the deployed gate — an absent\n * error must never be allowed to grant a green. */\nconst CHECKOUT_MARKER = \"src/routes\";\n\n/** Every placement a guard may legitimately take, most-preferred first.\n *\n * `src/routes/dev/+layout.server.ts` is the STRONGEST, and it is the fleet-wide\n * class-fix for \"dev routes ship in production\" (#717): one `load` doing\n * `if (!dev) error(404)` runs for every `/dev/*` route, `/dev/match/[uid]`\n * included. Reading only inside `/dev/match` reported such a site as \"the\n * matching twin would ship\" — untrue, and it penalised the better fix. It also\n * contradicted the deployed half of this same feature, which was deliberately\n * redesigned to survive that class-fix: see the `/health` comment at the\n * dev-guard step for why the 200 control was moved off `/dev/a11y-fixtures`.\n *\n * Below it, the `/dev/match` layout covers every child of the twin, present and\n * future; the page file is the narrowest placement and so the last one tried. */\nconst MATCH_GUARD_FILES = [\n \"src/routes/dev/+layout.server.ts\",\n \"src/routes/dev/match/+layout.server.ts\",\n \"src/routes/dev/match/[uid]/+page.server.ts\",\n] as const;\n\n/** Strip line and block comments, so the COMMON commented-out guard cannot\n * satisfy the match — an import left in place above `// if (!dev) error(404);`\n * is the likeliest real-world state, and it used to pass. String and template\n * literals are skipped so a `//` inside a URL is not read as a comment.\n *\n * This is a scanner, not a parser, and a REGEX LITERAL DEFEATS IT: a quote\n * character inside one — `/[a-z0-9']+/` — opens a string that never closes, and\n * everything to the next matching quote, comment text included, is preserved.\n * The residual direction is therefore UNDER-strip, not over-strip; an earlier\n * version of this comment asserted the opposite, and \"can only deny\" was not\n * something it could back.\n *\n * What stops that under-strip granting a pass today is `maskLiterals`, which\n * `carriesGuard` runs over this output and which misreads the same region the\n * same way — so the region is blanked rather than believed, and the observed\n * failure on such a file is a REFUSED real guard, not an accepted absent one.\n * Read that as one scanner's bug cancelling another's, not as a property\n * either function guarantees. */\nfunction stripComments(source: string): string {\n let out = \"\";\n for (let i = 0; i < source.length;) {\n const ch = source[i]!;\n const next = source[i + 1];\n if (ch === \"/\" && next === \"/\") {\n while (i < source.length && source[i] !== \"\\n\") i++;\n continue;\n }\n if (ch === \"/\" && next === \"*\") {\n i += 2;\n while (i < source.length && !(source[i] === \"*\" && source[i + 1] === \"/\")) i++;\n i += 2;\n continue;\n }\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n out += ch;\n i++;\n while (i < source.length) {\n if (source[i] === \"\\\\\") {\n out += source.slice(i, i + 2);\n i += 2;\n continue;\n }\n out += source[i]!;\n i++;\n if (source[i - 1] === ch) break;\n }\n continue;\n }\n out += ch;\n i++;\n }\n return out;\n}\n\n/** Blank the CONTENTS of string and template literals, keeping the quote\n * characters, the newlines and the overall length. Only the guard scan uses\n * this — never the `$app/environment` check, whose whole subject is an import\n * specifier and therefore is itself a string literal.\n *\n * It exists because `if (!dev) console.warn(\"this should throw error(404)\")`\n * read as a refusal. Masking can only ever REMOVE candidate refusal tokens, so\n * it can only ever deny. Its own edge case is the same one `stripComments` has\n * — a regex literal containing a quote character opens a literal that never\n * closes — and there too the failure is over-masking, which denies. */\nfunction maskLiterals(code: string): string {\n let out = \"\";\n for (let i = 0; i < code.length;) {\n const ch = code[i]!;\n if (ch !== '\"' && ch !== \"'\" && ch !== \"`\") {\n out += ch;\n i++;\n continue;\n }\n out += ch;\n i++;\n while (i < code.length) {\n const c = code[i]!;\n if (c === \"\\\\\") {\n out += \" \";\n i += 2;\n continue;\n }\n i++;\n if (c === ch) {\n out += c;\n break;\n }\n out += c === \"\\n\" ? \"\\n\" : \" \";\n }\n }\n return out;\n}\n\n/** The statement or block the `if (...)` ending at `from` actually governs: a\n * braced block, or the single statement up to its terminating `;` — or, under\n * ASI, the end of its line. Bracket depth is tracked so\n * `error(404, {\\n message: \"…\"\\n});` is ONE statement rather than three\n * fragments. Expects `code` to be comment-stripped and literal-masked, so every\n * bracket it sees is a real one. */\nfunction consequentAt(code: string, from: number): string {\n let i = from;\n while (i < code.length && /\\s/.test(code[i]!)) i++;\n const start = i;\n if (code[i] === \"{\") {\n let depth = 0;\n for (; i < code.length; i++) {\n if (code[i] === \"{\") depth++;\n else if (code[i] === \"}\" && --depth === 0) return code.slice(start, i + 1);\n }\n return code.slice(start);\n }\n let depth = 0;\n for (; i < code.length; i++) {\n const ch = code[i]!;\n if (ch === \"(\" || ch === \"[\" || ch === \"{\") depth++;\n else if (ch === \")\" || ch === \"]\" || ch === \"}\") {\n if (depth === 0) return code.slice(start, i);\n depth--;\n } else if (depth === 0 && (ch === \";\" || ch === \"\\n\")) return code.slice(start, i);\n }\n return code.slice(start);\n}\n\n/** An actual refusal: SvelteKit's `error(404, …)`, or any `throw`. */\nconst REFUSAL = /\\berror\\s*\\(\\s*404|\\bthrow\\b/;\n\n/** A guard is `$app/environment` + an `if (!dev)` **whose own branch refuses**.\n *\n * The three conditions used to be ANDed across the WHOLE file, with nothing\n * tying the refusal to the `if`. Two shapes passed that should not have: a\n * warn-only `if (!dev)` alongside the route's own unrelated `throw` (the exact\n * half-fix this comment used to claim was closed), and a refusal occurring only\n * inside a string. Reading the guard's consequent, on literal-masked source, is\n * strictly narrower than the old whole-file grep — every input it rejects, the\n * old predicate also had to reject on `$app/environment` or `if (!dev)`, or was\n * a false pass. It can therefore only deny, never grant.\n *\n * It is a contract with plan BC's `ROUTE_SERVER` template, which BC installs\n * and this pre-flight must accept; tests/recipes/launch.test.ts lifts that\n * template out of the plan and runs this predicate over it rather than\n * restating the pattern. */\nfunction carriesGuard(source: string): boolean {\n const code = stripComments(source);\n if (!code.includes(\"$app/environment\")) return false;\n const masked = maskLiterals(code);\n const guards = /if\\s*\\(\\s*!\\s*dev\\s*\\)/g;\n for (let m = guards.exec(masked); m; m = guards.exec(masked)) {\n if (REFUSAL.test(consequentAt(masked, m.index + m[0].length))) return true;\n }\n return false;\n}\n\n/**\n * Pre-flight: a checkout that still carries the matching twin must carry the\n * dev guard the `match-harness` recipe installs with it. Nothing is deleted at\n * launch — the harness stays for the next round — so the guard is the whole\n * mechanism keeping `/dev/match/*` off the production build.\n *\n * Filesystem only, and it runs BEFORE any GitHub write: a site that will fail\n * the deployed check should not first acquire branch protection and an audit.\n *\n * Liveness first. `src/routes` has to be there before a missing\n * `src/routes/dev/match` may be read as \"this site has no twin\" — otherwise the\n * pass is granted by an absence, and four separate operator errors produce that\n * same absence. See CHECKOUT_MARKER.\n */\nexport async function matchingDisposition(\n sitePath: string,\n): Promise<{ ok: boolean; message: string }> {\n if (!existsSync(join(sitePath, CHECKOUT_MARKER))) {\n return {\n ok: false,\n message: `${sitePath} has no ${CHECKOUT_MARKER} — this is not a SvelteKit checkout (never cloned, a stale clone, a mistyped site name, or the wrong working directory), so the twin's disposition cannot be established`,\n };\n }\n if (!existsSync(join(sitePath, MATCH_ROUTE_DIR))) {\n return {\n ok: true,\n message: `no ${MATCH_ROUTE_DIR} in this checkout (${CHECKOUT_MARKER} is present, so the absence is a real one)`,\n };\n }\n const read: string[] = [];\n for (const rel of MATCH_GUARD_FILES) {\n let source: string;\n try {\n source = await readFile(join(sitePath, rel), \"utf-8\");\n } catch {\n continue;\n }\n read.push(rel);\n if (carriesGuard(source)) {\n // Deliberately NOT \"the twin is guarded in production\". This function reads\n // source text on disk; it cannot observe the deployed build, and a field\n // that can only see configuration must not be named after the thing it\n // cannot see. The deployed behaviour is the `dev-guard` step's job.\n return {\n ok: true,\n message: `${rel} contains dev guard source — \\`if (!dev)\\` on \\`$app/environment\\` whose own branch refuses with a 404/throw (source text only; the deployed build is checked at the dev-guard step)`,\n };\n }\n }\n if (read.length === 0) {\n return {\n ok: false,\n message: `${MATCH_ROUTE_DIR} exists but none of ${MATCH_GUARD_FILES.join(\", \")} could be read — the twin's disposition cannot be established`,\n };\n }\n return {\n ok: false,\n message: `none of ${read.join(\", \")} carries an \\`if (!dev)\\` guard on \\`$app/environment\\` whose own branch refuses (\\`error(404\\` or \\`throw\\`), once comments are stripped and string contents masked — the matching twin would ship`,\n };\n}\n\n/** Undici's defaults are 300s headers + 300s body, and the body timeout is an\n * INACTIVITY timer — so a trickling origin can hold this open for ~10 minutes,\n * AFTER the GitHub writes and a full Lighthouse audit have already run.\n *\n * 15s is NOT the house number, and this comment used to imply it was by citing\n * `audits/function-health.ts`, which is 10s. The 15s call sites are\n * `audits/netlify-deploy.ts`, `github/gh-rest.ts`, `prismic/models/remote.ts`\n * and the inline ones in `prospect/pipeline.ts` and `prospect/http-probes.ts`;\n * `audits/function-health.ts`, `audits/form-e2e.ts`, `audits/browser.ts` and\n * `prospect/dns.ts` are 10s, `prospect/ownership.ts` 12s, `prospect/crawl.ts`\n * 20s. Each is set for its own workload — see the note on PRISMIC_TIMEOUT_MS.\n * 15s here is a bound on a two-request pre-send check, not a convention.\n *\n * An abort rejects, and dev-guard treats a rejection as a refusal, which is the\n * correct reading: an origin that will not answer has proved nothing. */\nconst PROBE_TIMEOUT_MS = 15_000;\n\nconst defaultProbe = async (url: string): Promise<{ status: number; body: string }> => {\n const res = await fetch(url, {\n redirect: \"follow\",\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),\n });\n return { status: res.status, body: await res.text() };\n};\n\n/**\n * Launch a site: bootstrap → first-audit → DRAFT a launch email. The M3\n * approve loop is what actually sends; `launch` never sends — it stops at a\n * dashboard-queued draft (`reportType: \"Launch\"`) carrying the just-audited\n * Lighthouse scores, so `sendOne`'s `report.lighthouse` guard passes.\n *\n * EXECUTION order, stopping on the first error or `failed` recipe:\n * 0. matchingDisposition — filesystem pre-flight, BEFORE any GitHub write.\n * 1. selfUpdating — Renovate + protection (platform auto-merge OFF; ci.yml is the starter's).\n * 2. audit — the Lighthouse scores that feed the draft are collected here.\n * 3. the Websites-row lookup, which supplies the url the next step probes.\n * 3b. dev-guard — the same twin, checked against the DEPLOYED url. It sits\n * BETWEEN collecting the scores and writing them, so a site that fails it\n * has been audited but leaves its Websites row untouched.\n * 4. writeAuditsToAirtable — the `audit --write-airtable` writer.\n * 5. createDraft — reportType \"Launch\", today's period, the audited scores.\n *\n * The REPORTED chain is deliberately not that order. The `audit` step is only\n * pushed once its write succeeds (:261), so the emitted steps read\n * `matching-disposition -> self-updating -> dev-guard -> audit -> draft`, which\n * is what the CLI prints and what tests/recipes/launch.test.ts asserts. Do not\n * renumber either list to match the other: one says when work happens, the\n * other says what has been proved. Only step 0 precedes a GitHub write —\n * dev-guard cannot, because it needs the row from step 3 to know the url.\n */\nexport async function launch(site: Site, deps: LaunchDeps = {}): Promise<LaunchResult> {\n const label = siteLabel(site);\n const bootstrap = deps.bootstrap ?? selfUpdating;\n const audit = deps.audit ?? runAudits;\n const base = deps.base ?? openBase(readAirtableConfig());\n const probe = deps.probe ?? defaultProbe;\n\n const steps: Array<{ name: string; result: LaunchStepResult }> = [];\n const stop = (): LaunchResult => ({ site: label, steps, complete: false });\n\n // 0. Matching disposition — filesystem, before any GitHub write.\n const disposition = await matchingDisposition(site.path);\n steps.push({\n name: \"matching-disposition\",\n result: disposition.ok\n ? { kind: \"probe\", message: disposition.message }\n : { kind: \"error\", message: disposition.message },\n });\n if (!disposition.ok) return stop();\n\n // 1. Bootstrap.\n let recipe: RecipeResult;\n try {\n recipe = await bootstrap(site);\n } catch (err) {\n steps.push({ name: \"self-updating\", result: errorOf(err) });\n return stop();\n }\n steps.push({ name: \"self-updating\", result: { kind: \"recipe\", result: recipe } });\n if (recipe.status === \"failed\") return stop();\n\n // 2. Audit + write scores back to Airtable.\n let results: AuditResult[];\n try {\n results = await audit(site);\n } catch (err) {\n steps.push({ name: \"audit\", result: errorOf(err) });\n return stop();\n }\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n if (!lhResult || !hasRealScores(lhResult)) {\n steps.push({\n name: \"audit\",\n result: { kind: \"error\", message: \"lighthouse audit produced no real scores\" },\n });\n return stop();\n }\n // The launch announcement renders a numeric score per category; a metric that\n // errored this run (now null from lighthouseScoresFromResult) keeps the prior\n // 0 behavior here rather than propagating null into the launch-email path. The\n // Airtable write path (write-audits-to-airtable) keeps the null → shows \"—\".\n const rawScores = lighthouseScoresFromResult(lhResult);\n const scores: LighthouseScores = {\n performance: rawScores.performance ?? 0,\n accessibility: rawScores.accessibility ?? 0,\n bestPractices: rawScores.bestPractices ?? 0,\n seo: rawScores.seo ?? 0,\n };\n\n const websites = await listWebsites(base);\n const target = websites.find((w) => siteSlug(w.name) === siteSlug(label));\n if (!target) {\n steps.push({\n name: \"audit\",\n result: { kind: \"error\", message: `no Websites row matched site \"${label}\"` },\n });\n return stop();\n }\n\n // 2b. The dev guard, on the DEPLOYED build. Two halves, both required:\n // /dev/match/home must 404 WITH this site's own error page, and /health\n // must answer 200. Without the second, a dead host, a wrong url and a\n // parked domain all \"pass\" the first — an absent error granting a green,\n // which is the one shape this fleet does not allow. Reading the deployed\n // url makes this a production-build check by construction; no local build\n // can substitute.\n //\n // The control is /health, NOT a /dev/* route. It used to be\n // /dev/a11y-fixtures, which made this gate require an unguarded dev page\n // to be publicly reachable in production — so the obvious class-fix for\n // \"dev routes ship in production\" (one src/routes/dev/+layout.server.ts\n // doing `if (!dev) error(404)`) would have taken the control down with it\n // and failed every correctly-guarded site. /health is a real production\n // endpoint shipped by the starter and present in all 23 starter-derived\n // repos, each declaring `prerender = false` — so a 200 from it is still a\n // live render/function path, not a static file served by the CDN.\n const origin = target.url.replace(/\\/+$/, \"\");\n let twin: { status: number; body: string };\n let control: { status: number; body: string };\n try {\n twin = await probe(`${origin}/dev/match/home`);\n control = await probe(`${origin}/health`);\n } catch (err) {\n steps.push({ name: \"dev-guard\", result: errorOf(err) });\n return stop();\n }\n if (control.status !== 200) {\n steps.push({\n name: \"dev-guard\",\n result: {\n kind: \"error\",\n message: `${origin}/health answered ${control.status}, not 200 — the twin's 404 proves nothing (host down, wrong url, or the site's /health function is not deploying, in which case the function-health audit is failing too)`,\n },\n });\n return stop();\n }\n const unguardedTell = twin.body.match(UNGUARDED_TWIN_MARKER);\n if (unguardedTell) {\n steps.push({\n name: \"dev-guard\",\n result: {\n kind: \"error\",\n message: `${origin}/dev/match/home answered ${twin.status} carrying the twin route's OWN \"${unguardedTell[0]}\" message — the twin is LIVE and merely has no assembly for the uid \"home\". That is not evidence of a dev guard.`,\n },\n });\n return stop();\n }\n if (twin.status !== 404 || !SITE_404_MARKER.test(twin.body)) {\n steps.push({\n name: \"dev-guard\",\n result: {\n kind: \"error\",\n message:\n twin.status === 404\n ? `${origin}/dev/match/home 404s, but not with this site's own error page — cannot tell the guard from a dead route`\n : `${origin}/dev/match/home answered ${twin.status} — the matching twin is live in production`,\n },\n });\n return stop();\n }\n steps.push({\n name: \"dev-guard\",\n result: {\n kind: \"probe\",\n message: `${origin}/dev/match/home 404 (site error page), /health 200`,\n },\n });\n\n try {\n const auditWrite = await writeAuditsToAirtable({\n base,\n websites,\n slug: siteSlug(target.name),\n results,\n });\n // #539 Phase 5: mirror the first-audit write-back. Launch is the ONE path\n // that writes a brand-new site's health, so without this its row reads empty\n // in the console until the next hourly sync.\n if (auditWrite.siteId && auditWrite.fields) {\n await deps.siteMirror?.health(auditWrite.siteId, auditWrite.fields);\n }\n } catch (err) {\n steps.push({ name: \"audit\", result: errorOf(err) });\n return stop();\n }\n steps.push({ name: \"audit\", result: { kind: \"audit\", results, scores } });\n\n // 3. Draft the launch email (reuses draft.ts's reportId/period scheme). DRAFTS\n // ONLY — the M3 approve loop sends it and flips Status on send.\n const today = new Date();\n const period = today.toISOString().slice(0, 7);\n const slug = siteSlug(target.name);\n\n let report: ReportRow;\n try {\n // Re-run dedupe: reuse an existing Launch row for this (site, period) instead\n // of stacking a second draft. findReportByPeriod is the same idempotency\n // lookup draft.ts documents (dashboard/digest point lookup).\n const existing = await findReportByPeriod(base, target.id, \"Launch\", period);\n if (existing) {\n // Reuse path: the row was created on a prior run with THAT run's scores. This\n // re-run just produced fresh audit scores AND will re-render the preview from\n // them — so refresh the row's Lighthouse cells (+ Completed on) to match,\n // otherwise the sent email (which reads the row) ships stale scores. The\n // create path already writes fresh scores via createDraft.\n await updateReportScores(base, existing.id, scores, today);\n // Mirror the same refresh — see the announce reuse path for why.\n await deps.reportMirror?.patch(existing.id, {\n lighthouse_performance: scores.performance,\n lighthouse_accessibility: scores.accessibility,\n lighthouse_best_practices: scores.bestPractices,\n lighthouse_seo: scores.seo,\n completed_on: today.toISOString().slice(0, 10),\n });\n report = existing;\n } else {\n report = await createDraft(\n base,\n draftInputFor(target, scores, today, period),\n deps.reportMirror?.created,\n );\n }\n } catch (err) {\n steps.push({ name: \"draft\", result: errorOf(err) });\n return stop();\n }\n\n // Mirror draft.ts:135-154 — render → upload \"Rendered HTML\" preview → flip\n // Draft ready. Without setDraftReady the draft never enters the approve queue\n // (every pending-approval gate requires draftReady true), so it can never be\n // approved or sent. The upload is a review convenience; the ready flag is the\n // critical step.\n try {\n const { html } = await renderReportHtml({\n siteName: target.name,\n siteUrl: target.url,\n reportType: \"Launch\",\n completedOn: today,\n lighthouse: scores,\n lastTestedDate: null,\n commentary: null,\n copy: resolveCopy(target),\n headerImageCid: `${slug}-header`,\n });\n // A preview-upload hiccup must NOT fail the launch — log and continue.\n try {\n await uploadAttachment(\n report.id,\n \"Rendered HTML\",\n html,\n `${slug}-${today.toISOString().slice(0, 10)}.html`,\n \"text/html\",\n );\n // The console preview reads the body from Turso, not the attachment.\n await deps.reportMirror?.body(report.id, html);\n } catch (uploadErr) {\n console.warn(\n `⚠ Launch preview upload skipped for ${target.name}: ${\n uploadErr instanceof Error ? uploadErr.message : String(uploadErr)\n }`,\n );\n }\n // Critical: NOT wrapped — a failure here must surface as a failed launch. queueDraft\n // supersedes any lower-tier (Maintenance/Testing) drafts queued for this site; a Launch is\n // top tier so it only stands down if another Launch/Announcement is already queued.\n await queueDraft(\n base,\n { id: report.id, siteId: target.id, reportType: \"Launch\" },\n deps.reportMirror,\n );\n } catch (err) {\n steps.push({ name: \"draft\", result: errorOf(err) });\n return stop();\n }\n\n steps.push({ name: \"draft\", result: { kind: \"draft\", report } });\n\n return { site: label, steps, complete: true };\n}\n\n/** Build the Launch `DraftInput`. reportId/period mirror `draftReportForSite`\n * (draft.ts) — do not invent a new id scheme. `today`/`period` are threaded in\n * from `launch()` so a single timestamp drives the render, the draft, the\n * dedupe lookup, and the preview filename. Launch reports have no period window\n * and no prior maintenance test, so periodStart/periodEnd/completedOn all\n * collapse to \"today\" and `lastTestedDate` is null. */\nfunction draftInputFor(\n target: WebsiteRow,\n scores: LighthouseScores,\n today: Date,\n period: string,\n): Parameters<typeof createDraft>[1] {\n const reportType = \"Launch\" as const;\n const reportId = `${target.name} — ${reportType} — ${today.toISOString().slice(0, 10)}`;\n return {\n reportId,\n siteId: target.id,\n reportType,\n period,\n periodStart: today,\n periodEnd: today,\n completedOn: today,\n lighthouse: scores,\n lastTestedDate: null,\n };\n}\n\nfunction errorOf(err: unknown): LaunchStepResult {\n return { kind: \"error\", message: err instanceof Error ? err.message : String(err) };\n}\n"],"mappings":"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","names":["depth"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/match-harness.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { matchHarness } from \"../../recipes/match-harness/index.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type MatchHarnessCommandOptions = {\n ref?: string;\n cand?: string;\n /** cac type-coerces a numeric option value, so this arrives as a NUMBER for\n * `--matrix 1440` and a string only when it contains a comma or a letter. */\n matrix?: string | number;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runMatchHarnessCommand(\n site: string | undefined,\n opts: MatchHarnessCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n // A bad --matrix must not silently become [NaN]: the matrix is the gate's\n // denominator, and a NaN viewport would produce a score nobody could read.\n //\n // Two things force the shape below. cac COERCES a numeric option value, so\n // `--matrix 1440` arrives as the number 1440 (`.split` would throw) and\n // `--matrix 0` as the number 0 — which is FALSY, so a truthiness test drops it\n // and the recipe silently applies the default [1440,834,390] and exits 0. So:\n // normalise to a string, and branch on PRESENCE, never on truthiness.\n const rawMatrix = opts.matrix === undefined ? undefined : String(opts.matrix);\n const matrix =\n rawMatrix === undefined ? undefined : rawMatrix.split(\",\").map((v) => Number(v.trim()));\n if (matrix !== undefined && matrix.some((v) => !Number.isFinite(v) || v <= 0)) {\n return {\n output: `match-harness: --matrix \"${rawMatrix}\" is not a list of viewports`,\n code: 1,\n };\n }\n\n const results = await runRecipeOverSites(\"match-harness\", sites, (s) =>\n matchHarness(s, {\n ref: opts.ref ?? \"\",\n ...(opts.cand !== undefined ? { cand: opts.cand } : {}),\n ...(matrix !== undefined ? { matrix } : {}),\n }),\n );\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAmBxB,SAAS,aAAa,GAAyB;AAC7C,MAAI,EAAE,WAAW,OAAQ,QAAO,IAAI,EAAE,IAAI,WAAW,EAAE,SAAS,EAAE;AAClE,MAAI,EAAE,WAAW,SAAU,QAAO,IAAI,EAAE,IAAI,aAAa,EAAE,SAAS,EAAE;AACtE,SAAO,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,MAAM;AAAA,EAAe,EAAE,SAAS,EAAE;AAC7E;AAEA,eAAsB,uBACpB,MACA,MAC2C;AAC3C,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI,QAAQ,IAAI;AAEvD,MAAI,QAAQ,MAAM,aAAa;AAAA,IAC7B,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IACxD,GAAI,KAAK,YAAY,SAAY,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF,CAAC;AAED,MAAI,UAAyB,CAAC;AAC9B,MAAI,KAAK,OAAO;AACd,UAAM,UAAU,KAAK,WAAW,aAAa;AAC7C,UAAM,OAAO,MAAM,kBAAkB,OAAO,EAAE,QAAQ,CAAC;AACvD,YAAQ,KAAK;AACb,cAAU,KAAK;AAAA,EACjB;AAUA,QAAM,YAAY,KAAK,WAAW,SAAY,SAAY,OAAO,KAAK,MAAM;AAC5E,QAAM,SACJ,cAAc,SAAY,SAAY,UAAU,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,CAAC,CAAC;AACxF,MAAI,WAAW,UAAa,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,CAAC,GAAG;AAC7E,WAAO;AAAA,MACL,QAAQ,4BAA4B,SAAS;AAAA,MAC7C,MAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UAAU,MAAM;AAAA,IAAmB;AAAA,IAAiB;AAAA,IAAO,CAAC,MAChE,aAAa,GAAG;AAAA,MACd,KAAK,KAAK,OAAO;AAAA,MACjB,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,MACrD,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3C,CAAC;AAAA,EACH;AAEA,QAAM,SAAS,QAAQ,IAAI,YAAY,EAAE,KAAK,IAAI;AAClD,QAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ,IAAI,IAAI;AAC9D,SAAO,EAAE,QAAQ,iBAAiB,QAAQ,OAAO,GAAG,KAAK;AAC3D;","names":[]}
|
package/dist/migrate-K4JETR36.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/prismic-ci.ts","../src/recipes/prismic-ci/index.ts","../src/recipes/prismic-ci/template.ts","../src/recipes/prismic-ci/cli-version.ts"],"sourcesContent":["// The fleet face of the `prismic-ci` recipe: land the Prismic model delivery\n// workflow across the fleet, as ONE PULL REQUEST PER REPOSITORY.\n//\n// The blast radius is fifteen live client repositories, and the failure this\n// file is written against is not a bad PR — the recipe's gates handle that, and\n// nothing here can push to a client's main. It is the QUIET one: a run that\n// touched nothing and exited 0, read as a finished rollout. Three facts only the\n// fleet layer can get wrong produce it, and each has a refusal below:\n//\n// - an inventory that resolved NOBODY prints \"0 applied, 0 noop, 0 failed.\"\n// and exits 0, which is indistinguishable from a fleet with nothing left to\n// do. The Airtable inventory is view-filtered; one filter change empties it\n// with no error anywhere.\n// - a site that could not be PREPARED is isolated into `skipped` by\n// `prepareFleetSites` (correctly — one bad row must not abort the fleet) and\n// then disappears from a summary built out of `prepared` alone. It gets a\n// `failed` ROW, because the count is the only thing a machine reads.\n// - a checkout holding `.git` and nothing else answers \"not a Prismic site\" to\n// every read inside it, so the site noops out of the rollout for good — a\n// fleet workdir reuses any non-empty directory forever. Guarded BEFORE the\n// recipe runs: a tree nobody established is not a tree to branch from.\n//\n// SEQUENTIAL, one site at a time, like every other fleet recipe command — see\n// `runRecipeOverSites`. Each site does git work in its own checkout plus a\n// handful of GitHub calls; there is no Airtable write on this path (so the\n// fleet's ≤4.5 req/s throttle does not apply) and nothing parallelism would buy\n// except a burst of writes against fifteen repositories at once.\nimport { readdir } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { prismicCi } from \"../../recipes/prismic-ci/index.js\";\nimport {\n REUSABLE_WORKFLOW_PIN,\n isPinResolved,\n type ReusableWorkflowPin,\n} from \"../../recipes/prismic-ci/template.js\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { siteLabel } from \"../../util/site.js\";\nimport { noWorkingTreeFailure } from \"../../util/working-tree.js\";\nimport { appendSkipNotice, prepareFleetSites, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\n\nexport type PrismicCiCommandOptions = {\n fleet?: string;\n workdir?: string;\n dry?: boolean;\n cwd?: string;\n};\n\n/** Injected only by tests, so the fleet layer's aggregation — counts, exit code,\n * refusals — is exercised without git, GitHub or a network. Production runs the\n * real recipe. */\nexport type PrismicCiCommandDeps = {\n runRecipe?: (site: Site) => Promise<RecipeResult>;\n /**\n * Which `reddoorla/.github` commit this run would pin. Injected only by tests;\n * production uses the shipped {@link REUSABLE_WORKFLOW_PIN}.\n *\n * It is injectable because the unresolved-pin refusal is a SAFETY behaviour\n * that must be tested in both directions at all times, and a test that reads\n * the shipped constant can only ever exercise whichever direction the constant\n * currently happens to be in. Two tests here did exactly that, and the release\n * that resolved the pin turned them from \"the refusal works\" into a red suite\n * with the refusal itself no longer covered by anything.\n */\n pin?: ReusableWorkflowPin;\n};\n\nconst messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\nconst failedRow = (site: string, notes: string): RecipeResult => ({\n recipe: \"prismic-ci\",\n site,\n status: \"failed\",\n commits: [],\n notes,\n});\n\n/**\n * One line per site plus a counts summary — the shape every fleet rollout\n * prints, so a sweep over fifteen repos is scannable.\n *\n * The banner above the counts is not decoration. A rollout in which every site\n * failed prints fifteen `[site] failed:` lines and then an arithmetic exercise;\n * the one fact an operator needs from that run — NOT ONE repository got the\n * workflow — has to be stated. It is keyed on \"sites were attempted and none was\n * applied\", not on \"nothing was applied\": a fleet that is already fully\n * delivered applies nothing every time it runs, and a banner that shouts at that\n * run is a banner nobody reads at the run above.\n */\nexport function formatPrismicCiResults(results: RecipeResult[]): string {\n const lines = results.map((r) => `[${r.site}] ${r.status}: ${r.notes ?? \"\"}`.trimEnd());\n const n = (s: RecipeResult[\"status\"]) => results.filter((r) => r.status === s).length;\n const failed = n(\"failed\");\n lines.push(\"\");\n if (failed > 0 && n(\"applied\") === 0) {\n lines.push(\n `⛔ NO SITE GOT THE DELIVERY WORKFLOW. ${failed} of ${results.length} site(s) failed and` +\n ` not one pull request was opened, so no site's model changes reach Prismic on merge.` +\n ` Do NOT read this run as a rollout.`,\n );\n lines.push(\"\");\n }\n lines.push(`${n(\"applied\")} applied, ${n(\"noop\")} noop, ${failed} failed.`);\n return lines.join(\"\\n\");\n}\n\n/**\n * Is there a checkout here at all, or the named reason there is not?\n *\n * Runs BEFORE the recipe, for both the reason in {@link noWorkingTreeFailure}\n * and one this command adds: the recipe's next steps create a branch, write a\n * file and commit it. A directory nobody managed to check out is not a tree to\n * do that in, and the answer the recipe would otherwise reach — \"not a Prismic\n * site\" — is the one answer that quietly removes the site from the rollout.\n *\n * A `readdir` that THROWS is its own failure and never the skip: a path that\n * does not exist, or one this process cannot read, is not a repo without\n * Prismic in it.\n */\nasync function checkoutFailure(repoRoot: string): Promise<string | null> {\n let entries: string[];\n try {\n entries = await readdir(repoRoot);\n } catch (e) {\n return `cannot read this checkout at ${repoRoot}: ${messageOf(e)}`;\n }\n return noWorkingTreeFailure(\n repoRoot,\n entries,\n \"No delivery workflow was proposed for it and no pull request was opened\",\n );\n}\n\n/**\n * Roll the delivery workflow out to one site or to the fleet.\n *\n * `resolveSites` is allowed to THROW (a positional site alongside `--fleet`, an\n * unsupported inventory extension, an Airtable read that failed). Those are \"the\n * fleet itself could not be established\", which has no per-site row to live in\n * and must not be reported as a rollout across zero sites; `bin.ts` prints the\n * message and exits with the error's own `exitCode`.\n */\nexport async function runPrismicCiCommand(\n site: string | undefined,\n opts: PrismicCiCommandOptions,\n deps: PrismicCiCommandDeps = {},\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n // Passed through because `--fleet airtable` derives each site's path from\n // it; without it the keyword inventory would resolve paths under a different\n // workdir than the one this run then prepares and commits in.\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n // NOTHING RESOLVED IS NOT A DELIVERED FLEET — the refusal `runFleetSweep` and\n // the token doctor both open with, for the same reason: the summary below\n // would print \"0 applied, 0 noop, 0 failed.\" over no rows at all and exit 0,\n // which is exactly what a finished rollout looks like.\n if (sites.length === 0) {\n return {\n output:\n `the inventory resolved NO SITES, so no site was offered the delivery workflow.` +\n ` This is not a delivered fleet — check the inventory (an Airtable view filter, an` +\n ` empty JSON file, a dynamic inventory returning []). Do NOT read this exit as a` +\n ` rollout.`,\n code: 1,\n };\n }\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const prep = await prepareFleetSites(sites, { workdir: opts.workdir ?? fleetWorkdir() });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n // THE PIN, checked here as well as in the recipe — not a second copy of the\n // gate but the same exported predicate on the same pin, asked at the one point\n // the recipe cannot answer for: a `--dry` run never reaches the recipe at all,\n // and a preview that promised fifteen pull requests the real run would refuse\n // is a preview of something that cannot happen.\n //\n // ONE pin object serves both this preview and the recipe below, so the preview\n // can never be answering about a different pin than the run it previews.\n const pin = deps.pin ?? REUSABLE_WORKFLOW_PIN;\n const pinUnresolved = !isPinResolved(pin)\n ? `⛔ the reusable-workflow pin is unresolved (${pin.sha}) — every site` +\n ` below would be REFUSED, not delivered. Publish and tag the workflow in` +\n ` reddoorla/.github, then set REUSABLE_WORKFLOW_PIN.`\n : \"\";\n\n if (opts.dry) {\n const lines = sites.map(\n (s) =>\n `[${siteLabel(s)}] would be offered the delivery workflow (no gate was evaluated:` +\n ` the secret, default-branch and already-delivered checks run only on a real run)`,\n );\n return {\n output: appendSkipNotice(\n [pinUnresolved, lines.join(\"\\n\")].filter((s) => s !== \"\").join(\"\\n\\n\"),\n skipped,\n ),\n code: pinUnresolved === \"\" ? 0 : 1,\n };\n }\n\n const run = deps.runRecipe ?? ((s: Site) => prismicCi(s, { pin }));\n const results = await runRecipeOverSites(\"prismic-ci\", sites, async (s) => {\n const failure = await checkoutFailure(s.path);\n return failure ? failedRow(siteLabel(s), failure) : run(s);\n });\n\n // A SITE THAT COULD NOT BE PREPARED IS A SITE THAT DID NOT GET THE WORKFLOW,\n // and it gets a row. `prepareFleetSites` isolates the clone failure into\n // `skipped`, which is right and is also exactly how a site disappears: a\n // summary walking `prepared` alone counts a fleet-wide clone outage — an\n // expired App token, GitHub down, a full disk — as \"0 applied, 0 failed\",\n // exit 0. The notice below stays; the row is what makes the site countable.\n for (const s of skipped) {\n results.push(\n failedRow(\n s.site,\n `could not prepare this checkout: ${s.reason}. No delivery workflow was proposed` +\n ` for it — this is NOT \"this site already has one\".`,\n ),\n );\n }\n\n return {\n output: appendSkipNotice(formatPrismicCiResults(results), skipped),\n code: results.some((r) => r.status === \"failed\") ? 1 : 0,\n };\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { readGitHubConfig } from \"../../github/config.js\";\nimport { makeGitHub, type GitHub } from \"../../github/gh.js\";\nimport { readPrismicConfig } from \"../../prismic/models/index.js\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport {\n branchName,\n checkoutBranch,\n commit as gitCommit,\n createBranch,\n currentBranch,\n deleteBranch,\n isWorkingTreeClean,\n push as gitPush,\n resolveOwnerRepo,\n} from \"../../util/git.js\";\nimport { siteLabel } from \"../../util/site.js\";\nimport { formatWithPrettier, resolveTargetPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n APPLY_BRANCH,\n PRISMIC_CI_WORKFLOW,\n REUSABLE_WORKFLOW_PIN,\n SECRET,\n WORKFLOW_PATH,\n isPinResolved,\n prismicCiWorkflow,\n type ReusableWorkflowPin,\n} from \"./template.js\";\nimport { MIN_CLI_VERSION, atLeast, readLockedCliVersion } from \"./cli-version.js\";\n\n/** Head-branch prefix of every PR this recipe opens. `branchName(\"prismic-ci\")`\n * produces `maint/prismic-ci-<timestamp>`, and the re-run guard matches on this\n * prefix — the same idiom as `findOpenSelfUpdatingPR`. */\nconst BRANCH_PREFIX = \"maint/prismic-ci-\";\n\n/** How long the target's prettier gets. Also what makes the fleet's default\n * spawn detach the child, so the kill reaches prettier and not just a wrapper.\n * Matches the pull-down path's budget in `src/prismic/models/write.ts`. */\nconst PRETTIER_TIMEOUT_MS = 60_000;\n\n/** Just the GitHub surface this recipe touches, so a test fake is five methods\n * rather than thirty. `makeGitHub()` satisfies it structurally. */\nexport type PrismicCiGitHub = Pick<\n GitHub,\n \"defaultBranch\" | \"secretExists\" | \"fileContentsOnBranch\" | \"openPullRequest\" | \"openPullRequests\"\n>;\n\nexport type PrismicCiDeps = {\n github?: PrismicCiGitHub;\n pushBranch?: (cwd: string, branch: string) => Promise<void>;\n spawn?: SpawnFn;\n /** Resolve the TARGET repo's own prettier. Injected so a test can assert the\n * absolute-path spawn without a populated `node_modules`. */\n resolvePrettier?: (repoRoot: string) => Promise<string | null>;\n /** Which `reddoorla/.github` commit the written workflow pins. Injected only\n * by tests; production uses the shipped pin. */\n pin?: ReusableWorkflowPin;\n};\n\nconst resultOf = (\n site: Site,\n status: RecipeResult[\"status\"],\n notes: string,\n commits: string[] = [],\n): RecipeResult => ({\n recipe: \"prismic-ci\",\n site: siteLabel(site),\n status,\n commits,\n notes,\n});\n\nconst messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\n/** Same normalization as `self-updating`'s config comparison: CRLF and a stray\n * trailing newline are not drift, and treating them as drift opens a needless\n * PR on every run. Any real content difference still differs. */\nfunction sameWorkflow(current: string, canonical: string): boolean {\n const norm = (s: string) => s.replace(/\\r\\n/g, \"\\n\").replace(/\\s+$/, \"\");\n return norm(current) === norm(canonical);\n}\n\n/**\n * Land the Prismic model delivery workflow in one repo, as a pull request.\n *\n * Everything before the first mutation is a gate, and the gates exist because\n * this recipe's output is the only path in the project that writes to a live\n * client's Prismic repository. Two of them are worth naming:\n *\n * - THE SECRET. A workflow whose `PRISMIC_WRITE_TOKEN` does not exist yet goes\n * red on the repo's first model PR. Landing 15 of those turns a rollout into\n * 15 red repos, so a definitively-absent secret FAILS the site rather than\n * nooping it: \"this site was skipped and a human must act\" is not the same\n * report as \"this site was already done\", and a rollout summary where those\n * two look alike is how a site silently never gets model delivery. A secret\n * whose existence could not be DETERMINED fails too, with different wording —\n * they are different facts and the operator's next move differs.\n *\n * - THE DEFAULT BRANCH. The reusable workflow's apply job hard-codes\n * `refs/heads/main`. On a repo whose default branch is anything else, every\n * model PR would merge green and never reach Prismic. Refuse instead.\n *\n * Nothing here can delete a model: the workflow it installs calls a module that\n * exports no delete path.\n */\nexport async function prismicCi(site: Site, deps: PrismicCiDeps = {}): Promise<RecipeResult> {\n // 1. Repo identity — from Airtable's 'Git repo' or, on a positional run, the\n // checkout's origin. `failed`, not `noop`: a rollout that silently skips a site\n // is the failure this whole recipe is guarding against one level up. The\n // strict shape check runs BEFORE the first `gh` call, so a typo'd or\n // attacker-controlled value can never be interpolated into an API path.\n let repo: string | null;\n try {\n repo = await resolveOwnerRepo(site);\n } catch (err) {\n // A malformed identity aborts before any `gh` write — surfaced as a recipe\n // failure rather than being interpolated into an API path.\n return resultOf(site, \"failed\", `could not resolve the repo identity: ${messageOf(err)}`);\n }\n if (!repo) {\n return resultOf(\n site,\n \"failed\",\n \"could not determine a GitHub repo for this site — set Airtable 'Git repo', or give \" +\n \"the checkout an origin remote whose URL is a GitHub owner/repo\",\n );\n }\n\n // 2. Is this a Prismic site? `readPrismicConfig` returns null for \"no Prismic\n // here\" and THROWS for a present-but-unreadable config — the distinction is\n // the point, so the throw becomes `failed` rather than being caught into\n // the skip branch.\n let isPrismic: boolean;\n try {\n isPrismic = (await readPrismicConfig(site.path)) !== null;\n } catch (err) {\n return resultOf(site, \"failed\", `could not read the Prismic config: ${messageOf(err)}`);\n }\n if (!isPrismic) return resultOf(site, \"noop\", \"not a Prismic site (no repositoryName) — skipped\");\n\n // 3. The pin. Checked before any network call: while it is unresolved NOTHING\n // can roll out, and 15 pointless API round-trips are not worth spending to\n // find that out per site.\n const pin = deps.pin ?? REUSABLE_WORKFLOW_PIN;\n if (!isPinResolved(pin)) {\n return resultOf(\n site,\n \"failed\",\n `reusable-workflow pin is unresolved (${pin.sha}) — publish + tag the workflow in ` +\n `reddoorla/.github and set REUSABLE_WORKFLOW_PIN before rolling out`,\n );\n }\n const workflow = pin === REUSABLE_WORKFLOW_PIN ? PRISMIC_CI_WORKFLOW : prismicCiWorkflow(pin);\n\n // 3b. THE BINARY THAT WILL ACTUALLY RUN. The reusable workflow does not\n // install this CLI — it runs `pnpm install --frozen-lockfile` and then the\n // site's OWN installed bin, so the version that executes is whatever this\n // repo's lockfile pins. Installing the caller next to a binary with no\n // `prismic-models` command yields a workflow that fails on the first model\n // PR, in a client repo, with an error naming an unknown command rather\n // than a rollout that ran too early.\n //\n // Local and cheap, so it sits with the pin gate ahead of the first network\n // call. And it is a REFUSAL on \"could not establish\", not a pass: the\n // asymmetry is deliberate, because the cost of waiting is a re-run and the\n // cost of being wrong is broken CI on someone else's repository.\n const locked = await readLockedCliVersion(site.path);\n if (!locked.ok) {\n return resultOf(\n site,\n \"failed\",\n `cannot establish which @reddoorla/maintenance version this repo's CI would run ` +\n `(${locked.reason}) — refusing to install a workflow that may not run`,\n );\n }\n if (!atLeast(locked.version, MIN_CLI_VERSION)) {\n return resultOf(\n site,\n \"failed\",\n `this repo's lockfile pins @reddoorla/maintenance ${locked.version}, which has no ` +\n `\\`prismic-models\\` command (first shipped in ${MIN_CLI_VERSION}) — bump the dependency ` +\n `and commit the lockfile first, or the workflow fails on its first model PR`,\n );\n }\n\n const ghConfig = readGitHubConfig();\n if (!deps.github && !ghConfig) return resultOf(site, \"failed\", \"GITHUB_TOKEN not set\");\n const gh = deps.github ?? makeGitHub({ token: ghConfig!.token });\n const spawn = deps.spawn ?? defaultSpawn;\n\n // 4. The secret. Absent and unreadable are separate answers with separate\n // wording; neither proceeds.\n let hasSecret: boolean;\n try {\n hasSecret = await gh.secretExists(repo, SECRET);\n } catch (err) {\n return resultOf(\n site,\n \"failed\",\n `could not determine whether ${repo} has the ${SECRET} secret (${messageOf(err)}) — ` +\n `refusing to install a workflow that may have no token`,\n );\n }\n if (!hasSecret) {\n return resultOf(\n site,\n \"failed\",\n `${repo} has no ${SECRET} Actions secret — the workflow reds the repo on its first ` +\n `model PR without it. Mint one, then: gh secret set ${SECRET} --repo ${repo}`,\n );\n }\n\n // 5. The default branch. NOT `.catch(() => \"main\")`: \"I could not read the\n // default branch\" and \"the default branch is main\" are opposite facts, and\n // guessing the second targets a PR (and an apply gate) at a branch nobody\n // confirmed.\n let base: string;\n try {\n base = await gh.defaultBranch(repo);\n } catch (err) {\n return resultOf(\n site,\n \"failed\",\n `could not read the default branch of ${repo}: ${messageOf(err)}`,\n );\n }\n if (base !== APPLY_BRANCH) {\n return resultOf(\n site,\n \"failed\",\n `default branch of ${repo} is ${base}, not ${APPLY_BRANCH} — the reusable workflow's ` +\n `apply job guards refs/heads/${APPLY_BRANCH}, so merged model changes would never reach Prismic`,\n );\n }\n\n // 6. Already delivered? Content-compared, not existence-compared, so a\n // present-but-STALE workflow (an older pin) is corrected rather than left\n // forever.\n const existing = await gh.fileContentsOnBranch(repo, base, WORKFLOW_PATH);\n if (existing !== null && sameWorkflow(existing, workflow)) {\n return resultOf(site, \"noop\", `delivery workflow already current on ${base}`);\n }\n\n // 7. Already proposed? Without this, every run before the PR merges opens\n // another one. `fileContentsOnBranch` also answers null for a read it could\n // not perform (gh exits non-zero on both a 404 and an auth failure), which\n // makes this the backstop for that collapse as well.\n const open = (await gh.openPullRequests(repo)).find((pr) => pr.headRef.startsWith(BRANCH_PREFIX));\n if (open) {\n return resultOf(site, \"noop\", `delivery workflow PR already open: ${open.url}`);\n }\n\n if (!(await isWorkingTreeClean(site.path))) {\n return resultOf(site, \"failed\", \"working tree not clean — commit or stash first\");\n }\n\n // Capture the operator's branch BEFORE creating ours so the `finally` can put\n // them back. Best-effort: if we cannot read it we skip the restore rather than\n // guess at a branch to check out.\n let original: string | null = null;\n try {\n original = await currentBranch(site.path);\n } catch {\n // Stays null (detached HEAD, git error) — the restore below is then skipped\n // rather than aiming a checkout at a branch name we had to invent.\n }\n const branch = branchName(\"prismic-ci\");\n const commits: string[] = [];\n let pushedOrOpened = false;\n\n try {\n await createBranch(site.path, branch);\n const dest = join(site.path, WORKFLOW_PATH);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, workflow, \"utf-8\");\n\n // Format with the SITE's own prettier, resolved POSITIVELY and run by\n // absolute path. `pnpm exec prettier` here would run a full `pnpm install`\n // in the client's repo first (an unrequested mutation of a live client repo\n // by a rollout that is supposed to add one file), and in a repo without\n // prettier would then format with the CALLING repo's binary and exit 0.\n const notes: string[] = [];\n const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(site.path);\n if (bin === null) {\n notes.push(PRETTIER_FLAG_NOTE);\n } else if (\n !(await formatWithPrettier(spawn, site.path, [WORKFLOW_PATH], {\n bin,\n timeoutMs: PRETTIER_TIMEOUT_MS,\n }))\n ) {\n notes.push(PRETTIER_FLAG_NOTE);\n } else if (!sameWorkflow(await readFile(dest, \"utf-8\"), workflow)) {\n // The site's prettier config disagrees with the template's shape (single\n // quotes, a different tab width). Harmless for the workflow — but the\n // step-6 comparison is byte-based, so this site will not recognise its own\n // delivered workflow as current and will propose it again after each\n // merge. Say so rather than let it be discovered as a mystery PR.\n notes.push(\n \"this site's prettier reformatted the workflow — re-runs will not match it as current\",\n );\n }\n\n const sha = await gitCommit(site.path, \"ci: deliver Prismic model changes from merged PRs\");\n if (!sha) {\n // Git saw no change: the checkout already holds this exact workflow even\n // though the default branch does not. Nothing to push and nothing to\n // review — and pushing a commit-less branch would fail at PR creation.\n return resultOf(\n site,\n \"noop\",\n `${WORKFLOW_PATH} already present and identical in the checkout`,\n );\n }\n commits.push(sha);\n pushedOrOpened = true;\n await (deps.pushBranch ?? gitPush)(site.path, branch);\n const pr = await gh.openPullRequest(repo, {\n head: branch,\n base,\n title: \"Deliver Prismic model changes from merged PRs\",\n body:\n \"Adds the `prismic-models` workflow. On a PR touching `customtypes/**` or \" +\n \"`src/lib/slices/**/model.json` it comments the model delta and writes nothing; \" +\n \"on merge to main it pushes those models to Prismic. It can create and update \" +\n \"models but never delete — a model present only in Prismic is reported, not touched.\",\n });\n notes.unshift(`opened PR ${pr.url}`);\n return resultOf(site, \"applied\", notes.join(\"; \"), commits);\n } catch (err) {\n return resultOf(site, \"failed\", messageOf(err), commits);\n } finally {\n // Restore the operator's branch on success AND on failure — otherwise a push\n // error strands the checkout on the recipe branch with an unpushed commit,\n // and the retry dies at createBranch. Best-effort; never masks the result.\n if (original !== null && original !== branch) {\n try {\n await checkoutBranch(site.path, original);\n // Nothing was pushed and no PR exists, so the local branch is litter and\n // a re-run would leave another. Only ever the branch WE created.\n if (!pushedOrOpened) await deleteBranch(site.path, branch);\n } catch (err) {\n console.warn(\n `warning: could not restore branch ${original} after prismic-ci: ${messageOf(err)}`,\n );\n }\n }\n }\n}\n","/**\n * The caller workflow every Prismic site gets.\n *\n * Fully generic — no per-site values — because the token is the site's own\n * `PRISMIC_WRITE_TOKEN` secret, the name every site's code already reads.\n * Contrast `ci.yml`, which is deliberately NOT templated anywhere in this\n * package because it carries per-site `netlify-site:` / `node-version:` values\n * a byte template would strip fleet-wide.\n */\n\n/** Where the file lands in a site repo. Also the `gh api .../contents/<path>`\n * segment, so it must stay traversal-free (`assertUrlSegment` re-checks). */\nexport const WORKFLOW_PATH = \".github/workflows/prismic-models.yml\";\n\n/** The Actions secret the reusable workflow declares as `required: true`, and\n * the environment variable the CLI reads. Spelled ONCE, here, because the\n * failure mode of a second spelling is silent: a caller passing a name the\n * callee never declared supplies nothing, and the callee's required secret\n * arrives empty at the one step that writes to a live client's Prismic repo. */\nexport const SECRET = \"PRISMIC_WRITE_TOKEN\";\n\n/** The reusable workflow, without a ref. */\nexport const REUSABLE_WORKFLOW = \"reddoorla/.github/.github/workflows/prismic-models.yml\";\n\n/**\n * The ONE branch this delivery path can run its apply job on.\n *\n * Not a preference — the reusable workflow's apply job hard-codes\n * `github.ref == 'refs/heads/main'`. On a repo whose default branch is anything\n * else, a merged model PR would produce a green check and never reach Prismic,\n * which is why the recipe refuses such a repo instead of installing a workflow\n * that silently cannot fire.\n */\nexport const APPLY_BRANCH = \"main\";\n\n/** A commit in `reddoorla/.github`, plus the release tag that commit carries.\n * Both are written into the workflow: the SHA is what Actions resolves, the tag\n * is what a human (and Renovate's github-actions manager) reads. */\nexport type ReusableWorkflowPin = { sha: string; tag: string };\n\n/**\n * What the shipped pin says while the reusable workflow has not been published\n * to `reddoorla/.github` and tagged yet (plan Task 25).\n *\n * DELIBERATELY NOT 40 HEX CHARACTERS. A placeholder shaped like a real SHA is\n * indistinguishable from a real one in review, and would install a workflow\n * referencing a commit that does not exist into 15 client repositories — where\n * it fails at workflow-load time, on every model PR, with an error that names\n * neither this file nor the reason. Unresolved must be unmistakable, and\n * {@link isPinResolved} is what the recipe gates on.\n */\nexport const UNRESOLVED_PIN_SHA = \"UNRESOLVED-publish-and-tag-reddoorla-dot-github-first\";\n\n/**\n * The pin the fleet rolls out.\n *\n * RESOLVED 2026-08-14. `reddoorla/.github#30` merged as\n * 558395431ddcb481ecba3dd84b78b38c338cfa03 and that commit was tagged v1.4.0;\n * `gh api repos/reddoorla/.github/commits/v1.4.0 --jq .sha` returns the SHA\n * below. `validate` (the digest-pinning + Renovate-preset check that is the\n * only CI in that repo) passed on the merge commit.\n *\n * TO RE-RESOLVE after a future `reddoorla/.github` release: set `sha` to\n * `gh api repos/reddoorla/.github/commits/<tag> --jq .sha` and `tag` to that\n * tag — then VERIFY PROPAGATION rather than assuming it. Renovate does bump\n * this ref: 17 site repos took v1.2.0 -> v1.3.0 in the weeks after v1.3.0\n * was tagged 2026-07-14, on Renovate's weekly `renovate/all-minor-patch`\n * branches (some of which were already open before the tag, so a PR's\n * creation date can predate the version it ends up bumping). It runs self-hosted here and its author identity has already\n * changed once mid-window (operator PAT, authoring as `tucksravin`, until\n * 2026-08-02; `reddoor-renovate[bot]` since), so no single author filter\n * matches all 17 and `app/renovate` matches none of them. Search the\n * `renovate/*` head branch, not an author; an empty author-filtered search\n * is not evidence. But it proposed neither\n * of the last two tags for `ci.yml`: v1.4.0 (2026-08-14) was never offered,\n * and v1.4.1 (2026-09-01) reached the fleet only because ci.yml was swept BY\n * HAND across 21 repos that day (beachfront-dentistry#36). Treat propagation\n * as three steps to run, not a guarantee: (1) re-resolve this constant,\n * (2) release @reddoorla/maintenance, (3) `reddoor-maint prismic-ci --fleet\n * airtable` — the already-delivered gate content-compares the installed file\n * and opens a corrective PR per stale repo — plus ONE POSITIONAL RUN PER\n * PRE-LAUNCH SITE, which the Airtable inventory excludes\n * (`src/inventory/airtable.ts:50` filters `building`/`launching`).\n */\nexport const REUSABLE_WORKFLOW_PIN: ReusableWorkflowPin = {\n sha: \"558395431ddcb481ecba3dd84b78b38c338cfa03\",\n tag: \"v1.4.0\",\n};\n\n/**\n * Is this pin a real commit reference?\n *\n * A full 40-hex SHA and nothing else. Not a tag, not a branch: this repo pins\n * every `uses:` to a commit (see `src/recipes/sync-configs/templates.ts`, whose\n * renovate template spells out why — a retagged `@v3` runs attacker code with\n * whatever token the workflow holds). Do not assume the pin bumps itself:\n * Renovate has proposed this ref in the past but did not for either of the\n * last two tags — see the propagation note on\n * {@link REUSABLE_WORKFLOW_PIN} above.\n */\nexport function isPinResolved(pin: ReusableWorkflowPin): boolean {\n return /^[0-9a-f]{40}$/.test(pin.sha);\n}\n\n/**\n * Render the caller workflow for a pin.\n *\n * Path-filtered on both triggers: this must not run on every commit, only when\n * a model changes.\n */\nexport function prismicCiWorkflow(pin: ReusableWorkflowPin): string {\n return `# Delivers this site's Prismic model changes. Managed by @reddoorla/maintenance\n# (\\`reddoor-maint prismic-ci\\`) — change it there and re-run, not here.\n#\n# On a PR touching a model: comment the delta, write nothing.\n# On merge to ${APPLY_BRANCH}: push those models to Prismic. Never delete.\n#\n# THE BRANCH FILTER ON \\`push:\\` IS LOAD-BEARING, not tidiness. The reusable\n# workflow's apply job also guards \\`github.ref == 'refs/heads/${APPLY_BRANCH}'\\`,\n# and its comment calls this the other half of that gate: an unfiltered\n# \\`on: push:\\` fires on every branch AND every tag, so a feature branch's models\n# would reach production with no pull request and therefore no review anywhere in\n# the sequence. Keep both halves.\nname: prismic-models\n\non:\n # No branch filter here, deliberately: a model PR into any base deserves its\n # delta comment, and the dry job is INCAPABLE of writing — it never passes\n # \\`--apply\\`, and it holds no job that does.\n pull_request:\n paths:\n - \"customtypes/**\"\n - \"src/lib/slices/**/model.json\"\n push:\n branches: [${APPLY_BRANCH}]\n paths:\n - \"customtypes/**\"\n - \"src/lib/slices/**/model.json\"\n\njobs:\n prismic-models:\n # A called workflow's jobs can only NARROW what the caller granted, so the\n # PR-comment permission has to be granted here as well as there. Nothing in\n # either job pushes code, so \\`contents\\` stays read.\n permissions:\n contents: read\n pull-requests: write\n uses: ${REUSABLE_WORKFLOW}@${pin.sha} # ${pin.tag}\n secrets:\n # Spelled exactly as the reusable workflow declares it. A workflow-local\n # alias would not be a rename — it would be an undeclared secret, and the\n # required one would arrive empty.\n ${SECRET}: \\${{ secrets.${SECRET} }}\n`;\n}\n\n/** The workflow as currently shipped. Unusable — and refused by the recipe —\n * until {@link REUSABLE_WORKFLOW_PIN} is resolved. */\nexport const PRISMIC_CI_WORKFLOW = prismicCiWorkflow(REUSABLE_WORKFLOW_PIN);\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/**\n * The first published version whose `reddoor-maint` bin carries `prismic-models`.\n *\n * VERIFIED BY INSTALLING IT, not by reading a changelog: `npm i\n * @reddoorla/maintenance@0.83.0` in an empty directory, then `reddoor-maint\n * prismic-models --help`, prints the command's usage. Every earlier published\n * version — 0.82.0 included — exits with \"unknown command\".\n *\n * This matters because the reusable workflow does NOT install the CLI. It runs\n * `pnpm install --frozen-lockfile` and then the site's own installed bin, so the\n * version that executes in a client repo is whatever that repo's lockfile pins.\n * Installing the caller workflow next to an older binary produces a workflow\n * that fails at the first model PR, in a client repo, with an error about an\n * unknown command rather than about a rollout that ran too early.\n */\nexport const MIN_CLI_VERSION = \"0.83.0\";\n\n/** The dependency whose version decides whether the workflow can run at all. */\nconst PACKAGE = \"@reddoorla/maintenance\";\n\n/**\n * `version >= min`, compared NUMERICALLY, position by position.\n *\n * Not a string comparison, and not `localeCompare`: as strings `\"0.9.0\" >\n * \"0.83.0\"`, so a lexicographic gate waves through a site pinned seventy-four\n * releases before the command existed. That is the single most likely way for\n * this gate to be wrong while looking right.\n *\n * A prerelease (`0.83.0-beta.1`) sorts BELOW the release it precedes, per\n * semver. The fleet publishes no prereleases today; the rule is here so that if\n * one ever appears it is not silently treated as the release.\n */\nexport function atLeast(version: string, min: string): boolean {\n const parse = (v: string): { nums: number[]; pre: boolean } => {\n const [core = \"\", ...rest] = v.split(\"-\");\n return {\n nums: core.split(\".\").map((n) => Number.parseInt(n, 10) || 0),\n pre: rest.length > 0,\n };\n };\n const a = parse(version);\n const b = parse(min);\n for (let i = 0; i < 3; i++) {\n const av = a.nums[i] ?? 0;\n const bv = b.nums[i] ?? 0;\n if (av !== bv) return av > bv;\n }\n // Cores equal: a prerelease of the minimum is below it; anything else is equal.\n if (a.pre && !b.pre) return false;\n return true;\n}\n\n/** Either the version pnpm resolved, or WHY that could not be established.\n * The two are deliberately different shapes: a caller cannot read \"I could not\n * tell\" as a version by forgetting to check a flag. */\nexport type LockedCliVersion = { ok: true; version: string } | { ok: false; reason: string };\n\n/**\n * The version of {@link PACKAGE} the repo's committed pnpm lockfile resolves.\n *\n * Reads the LOCKFILE, never `package.json`. The declared range is not what\n * executes: espada declares `^0.81.0` and its lockfile resolves 0.69.0, and CI\n * installs `--frozen-lockfile`. A gate reading the range would pass a site whose\n * install is two dozen releases behind it.\n *\n * Parsed with a targeted scan rather than a YAML dependency — this needs one\n * field out of one block, the repo carries no YAML parser, and adding one to\n * read a single `version:` line is not a trade worth making. The scan is\n * deliberately narrow: it reads only `importers:` entries (what is installed),\n * never the `packages:`/`snapshots:` sections, which list every transitively\n * resolvable version and would happily report one nobody installed.\n *\n * Every failure is a NAMED reason, never a version and never a silent default.\n * A gate that cannot tell \"too old\" from \"could not read\" reports one as the\n * other, which is the absent-vs-unreadable collapse this whole feature exists to\n * prevent — arriving here at the last step before writing to a client repo.\n */\nexport async function readLockedCliVersion(repoRoot: string): Promise<LockedCliVersion> {\n let raw: string;\n try {\n raw = await readFile(join(repoRoot, \"pnpm-lock.yaml\"), \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n // ENOENT is the only error meaning \"this repo has no lockfile\". EACCES and\n // friends mean it is there and unreadable — a different answer, and one that\n // must not read as \"not a pnpm site\".\n if (code === \"ENOENT\") {\n return {\n ok: false,\n reason: `no pnpm-lock.yaml in the checkout — cannot tell which ${PACKAGE} version CI would run`,\n };\n }\n return { ok: false, reason: `pnpm-lock.yaml is present but unreadable (${String(err)})` };\n }\n\n // Only inside `importers:` — see the note above on packages:/snapshots:.\n const lines = raw.split(\"\\n\");\n const importersAt = lines.findIndex((l) => /^importers:\\s*$/.test(l));\n if (importersAt === -1) {\n return {\n ok: false,\n reason: \"pnpm-lock.yaml has no `importers:` section — unrecognised lockfile shape\",\n };\n }\n const end = lines.findIndex((l, i) => i > importersAt && /^[a-zA-Z]/.test(l));\n const importers = lines.slice(importersAt + 1, end === -1 ? lines.length : end);\n\n const found = new Set<string>();\n for (let i = 0; i < importers.length; i++) {\n if (!new RegExp(`^\\\\s+'?${PACKAGE.replace(\"/\", \"\\\\/\")}'?:\\\\s*$`).test(importers[i]!)) continue;\n // The entry's own `version:` sits within the next few lines, alongside\n // `specifier:`. Bounded so a malformed block cannot walk into the next\n // dependency and attribute its version to this one.\n for (let j = i + 1; j < Math.min(i + 4, importers.length); j++) {\n const m = /^\\s+version:\\s*(\\S+)\\s*$/.exec(importers[j]!);\n if (!m) continue;\n // Strip the peer-dependency suffix: real fleet lockfiles run it past 400\n // characters after the semver.\n found.add(m[1]!.split(\"(\")[0]!);\n break;\n }\n }\n\n if (found.size === 0) {\n return { ok: false, reason: `${PACKAGE} is not a dependency in this repo's lockfile` };\n }\n if (found.size > 1) {\n // Picking one would be a coin flip deciding whether a client repo gets a\n // workflow its binary cannot run.\n return {\n ok: false,\n reason: `lockfile resolves ${PACKAGE} to more than one version (${[...found].sort().join(\", \")}) — refusing to guess which one CI would run`,\n };\n }\n return { ok: true, version: [...found][0]! };\n}\n"],"mappings":"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","names":["readFile","join","join","readFile","messageOf"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/prospect-audit.ts","../src/prospect/report-url.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { hostnameOf, isHttpUrl, isPrivateOrLoopbackHost } from \"../../util/url.js\";\nimport { reportUrl, reportPrintUrl } from \"../../prospect/report-url.js\";\nimport type { ProspectAuditStatus } from \"../../db/prospect-audits.js\";\nimport type { PipelineDeps, StageName } from \"../../prospect/pipeline.js\";\nimport type { ProspectAuditResult } from \"../../prospect/types.js\";\nimport type { SendAuditEmailResult } from \"../../prospect/email.js\";\nimport type { SiteGoal } from \"../../prospect/goals.js\";\n\n/** The operator-selectable goals, in the order the dispatch dropdown lists\n * them. `unknown` is deliberately absent: it is a finding the audit can reach\n * on its own, never something an operator would choose. */\nexport const GOALS = [\n \"book\",\n \"enquire\",\n \"call\",\n \"visit\",\n \"buy\",\n \"demo\",\n \"partner\",\n] as const satisfies readonly SiteGoal[];\n\nfunction isSiteGoal(value: string): value is SiteGoal {\n return (GOALS as readonly string[]).includes(value);\n}\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /**\n * The one action the site should produce from a visitor, chosen by the\n * operator instead of inferred from the site.\n *\n * Worth overriding because the operator knows things the site does not say.\n * A practice that wants online bookings but has never built one reads as\n * \"call\" from its own pages, and grading it as \"call\" would score it well for\n * doing the thing they are trying to stop doing.\n */\n goal?: string;\n /** Comma-separated competitor domains. */\n competitors?: string;\n /** cac sets this false for `--no-probes`. */\n probes?: boolean;\n out?: string;\n json?: boolean;\n /**\n * Email the internal sheet (scores, what wasn't measured and why, top\n * fixes, and the shareable link) to `PROSPECT_AUDIT_RECIPIENTS`, once the\n * report is rendered and the `--out`/Turso delivery attempts finish. A\n * missing/empty recipients var, or a Resend failure, is a warning — never a\n * crash — because by the time this runs the audit is already delivered (or\n * recovered) by the code above it.\n */\n email?: boolean;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n};\n\ntype RecoveryWrite = { htmlPath: string; jsonPath: string };\n\nfunction fail(message: string): { output: string; code: number } {\n return { output: message, code: 2 };\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction scoreLine(label: string, value: number | null): string {\n return `${label.padEnd(14)} ${value === null ? \"not measured\" : String(value).padStart(3)}`;\n}\n\n/** Item 3: the `status` column exists to answer, without deserializing the\n * whole result_json, whether this was a clean run or a degraded one. crawl\n * is excluded — it's the one fatal stage (see pipeline.ts), so by the time a\n * ProspectAuditResult exists it was always ok. Every other stage's\n * StageResult already collapses \"failed\" and \"deliberately skipped\"\n * (--no-probes, the checks→analyze cascade) into the same `ok: false` — both\n * mean the report has a \"not measured\" section, so both count as partial\n * here too. */\nfunction auditStatus(result: ProspectAuditResult): ProspectAuditStatus {\n const allStagesOk =\n result.checks.ok && result.lighthouse.ok && result.analyze.ok && result.probes.ok;\n return allStagesOk ? \"complete\" : \"partial\";\n}\n\n/** Filesystem-safe, never empty. Collapses anything outside [a-z0-9.-] to a\n * single dash so a hostname becomes a sane filename fragment. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\n/**\n * Last-resort write when NEITHER `--out` nor the database persist made it —\n * the one situation a paid, already-run audit must never end in silently. Lands\n * in `reports/`, mirroring header-image.ts's own convention for CLI-written\n * artifacts (mkdir -p, resolved against cwd), rather than a bare file dropped\n * into whatever directory the operator happened to be standing in when they\n * ran the command — often unrelated to this repo at all in fleet-tool usage.\n */\nasync function writeRecoveryFiles(\n result: ProspectAuditResult,\n html: string,\n): Promise<RecoveryWrite> {\n const dir = resolve(\"reports\");\n await mkdir(dir, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const base = `prospect-audit-${slugify(hostnameOf(result.url))}-${stamp}`;\n const htmlPath = resolve(dir, `${base}.html`);\n const jsonPath = resolve(dir, `${base}.json`);\n await writeFile(htmlPath, html, \"utf-8\");\n await writeFile(jsonPath, JSON.stringify(result, null, 2), \"utf-8\");\n return { htmlPath, jsonPath };\n}\n\nfunction summarize(\n result: ProspectAuditResult,\n link: string | null,\n file: string | null,\n warnings: string[],\n recovery: RecoveryWrite | null,\n email: SendAuditEmailResult | null,\n): string {\n const lines = [\n `Prospect audit — ${result.businessName ?? result.url}`,\n \"\",\n scoreLine(\"Findability\", result.scores.findability),\n scoreLine(\"Readability\", result.scores.readability),\n scoreLine(\"Answers\", result.scores.answers),\n // No AI Visibility line: the score was removed from every presentation\n // surface (web, print, email, here) — the measurement lives on as the\n // report's receipts section, not as a number that reads as ours to move.\n ];\n for (const [name, stage] of [\n [\"checks\", result.checks],\n [\"lighthouse\", result.lighthouse],\n [\"analyze\", result.analyze],\n [\"probes\", result.probes],\n ] as const) {\n if (!stage.ok) lines.push(` ! ${name} not measured — ${stage.error}`);\n }\n if (file) lines.push(\"\", `Report written to ${file}`);\n if (link) lines.push(\"\", `Shareable link: ${link}`);\n if (email?.sent) {\n lines.push(\"\", `Emailed to ${email.recipients.join(\", \")} (${email.messageId})`);\n }\n for (const w of warnings) lines.push(\"\", `! ${w}`);\n if (recovery) {\n lines.push(\n \"\",\n \"Neither --out nor the database took this report — wrote a recovery copy instead:\",\n ` ${recovery.htmlPath}`,\n ` ${recovery.jsonPath}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Run one prospect audit end to end. Progress goes to stderr so `--json` stdout\n * stays pipeable. Persistence needs Turso; without it `--out` is mandatory,\n * because an audit nobody can read afterwards is just a bill.\n *\n * By the time `runProspectAudit` resolves, the money is already spent — only\n * the crawl is fatal, every other stage degrades rather than throwing. So the\n * `--out` write and the database persist below are each wrapped in their own\n * try/catch and attempted independently: a bad `--out` path must not cost a\n * working Turso persist (or vice versa), and if BOTH fail — or persist fails\n * with no `--out` given at all — a recovery copy goes to `reports/` rather\n * than the run ending with nothing anywhere. `--json` only changes the SHAPE\n * of stdout; it still runs the same `--out`/persist/recovery logic beneath it.\n */\nexport async function runProspectAuditCommand(\n url: string,\n opts: ProspectAuditCliOptions,\n): Promise<{ output: string; code: number }> {\n if (!isHttpUrl(url)) {\n return fail(`\"${url}\" is not a URL. Pass the full address, e.g. https://example.com`);\n }\n // #612 review: this validated only the SHAPE. The sole private-address guard\n // was in `triggerProspectAudit` — one layer at the far end of the chain — so\n // anyone running the CLI directly, or any future second caller, had none.\n // `crawlSite` now refuses too; this keeps the refusal a clean message instead\n // of a thrown crawl error.\n if (isPrivateOrLoopbackHost(new URL(url).hostname)) {\n return fail(`\"${url}\" is a private or loopback address — refusing to audit it.`);\n }\n const canPersist = Boolean(process.env.TURSO_DATABASE_URL);\n if (!canPersist && !opts.out) {\n return fail(\n \"No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials.\",\n );\n }\n\n const { runProspectAudit } = await import(\"../../prospect/pipeline.js\");\n const onStage = (name: StageName, status: \"start\" | \"ok\" | \"fail\", detail?: string): void => {\n if (status === \"start\") console.error(`… ${name}`);\n else if (status === \"ok\") console.error(`✓ ${name}`);\n else console.error(`! ${name} — ${detail ?? \"failed\"}`);\n };\n\n const business = opts.business?.trim();\n const goal = opts.goal?.trim();\n if (goal !== undefined && goal !== \"\" && !isSiteGoal(goal)) {\n return fail(`--goal must be one of: ${GOALS.join(\", \")}`);\n }\n\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n // An empty string is what a blank dispatch field sends; it must read as\n // \"not supplied\" so the model's own inference still runs, rather than\n // overriding it with nothing.\n ...(goal ? { goal } : {}),\n ...(opts.competitors\n ? {\n competitors: opts.competitors\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean),\n }\n : {}),\n ...(opts.probes === false ? { probes: false } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\n\n const warnings: string[] = [];\n\n let file: string | null = null;\n if (opts.out) {\n try {\n await writeFile(opts.out, html, \"utf-8\");\n file = opts.out;\n } catch (err) {\n warnings.push(`Could not write --out (${opts.out}): ${errorMessage(err)}`);\n }\n }\n\n let link: string | null = null;\n let token: string | null = null;\n let auditId: string | null = null;\n if (canPersist) {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { createProspectAudit } = await import(\"../../db/prospect-audits.js\");\n const db = await openDb(readDbConfig());\n const created = await createProspectAudit(db, {\n url: result.url,\n // Map at the boundary: ProspectAuditResult.businessName is the field\n // name (Item 2 — it's a resolved NAME, not a description); the\n // `prospect_audits.business` column keeps its existing name, since\n // renaming a column needs its own migration for no benefit here.\n business: result.businessName,\n status: auditStatus(result),\n resultJson: JSON.stringify(result),\n });\n auditId = created.id;\n token = created.token;\n link = reportUrl(token);\n } catch (err) {\n warnings.push(`Could not save to the database: ${errorMessage(err)}`);\n }\n }\n\n const delivered = file !== null || link !== null;\n const recovery = delivered ? null : await writeRecoveryFiles(result, html);\n const code = delivered ? 0 : 1;\n\n // The audit is already delivered (or recovered) above — an email failure\n // must never take that away. Same independent try/catch shape as the\n // --out write and the Turso persist: record a warning, keep going, never\n // touch `code`. Dynamically imported for the same reason db/client.js and\n // prospect/render.js are above: it reaches `resend` (a devDependency) and\n // must stay out of bin.js's static import graph (see bin.ts's\n // central-dep-blocker comment).\n // The PDF leave-behind, printed from the website's print route — a document\n // designed for paper, not the interactive report with its evidence folded\n // away behind disclosures.\n //\n // Best-effort by design. Every other stage in this pipeline degrades rather\n // than throwing, and an attachment is not worth losing a delivered report\n // over: if this fails the email still goes, with the link, and a warning says\n // the PDF is missing. It also needs a token — with no persisted report there\n // is no page to print.\n let pdf: Buffer | null = null;\n if (opts.email && token) {\n try {\n const { renderReportPdf } = await import(\"../../prospect/pdf.js\");\n pdf = await renderReportPdf(reportPrintUrl(token));\n } catch (err) {\n warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);\n }\n }\n\n let email: SendAuditEmailResult | null = null;\n if (opts.email) {\n try {\n const { sendAuditEmail, parseProspectAuditRecipients } =\n await import(\"../../prospect/email.js\");\n const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);\n email = await sendAuditEmail(result, { link, recipients, auditId, pdf });\n if (!email.sent) {\n warnings.push(`Could not email the audit sheet: ${email.reason}`);\n }\n } catch (err) {\n warnings.push(`Could not email the audit sheet: ${errorMessage(err)}`);\n }\n }\n\n if (opts.json) {\n return {\n output: JSON.stringify(\n { result, out: file, link, token, recovery, warnings, email },\n null,\n 2,\n ),\n code,\n };\n }\n\n return { output: summarize(result, link, file, warnings, recovery, email), code };\n}\n","/** Where the prospect-facing report lives now: a real, branded route on the\n * marketing site rather than generated HTML on the ops app's domain. */\nexport const DEFAULT_REPORT_BASE_URL = \"https://reddoorla.com\";\n\n/**\n * Resolve the public report origin.\n *\n * Deliberately its own variable rather than reusing `DASHBOARD_BASE_URL`. That\n * one addresses operators and points at the ops app; this one addresses a\n * prospect and points at reddoorla.com. They are different audiences on\n * different domains, and sharing a variable would mean one of them silently\n * moving the day the other is repointed.\n */\nexport function resolveReportBaseUrl(raw: string | undefined): string {\n return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\\/$/, \"\");\n}\n\n/** The link a prospect opens. */\nexport function reportUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${resolveReportBaseUrl(raw)}/audit/${token}`;\n}\n\n/** The print-designed variant, which the runner renders to a PDF. Same token,\n * same guards — a separate document, not a stylesheet over the page. */\nexport function reportPrintUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${reportUrl(token, raw)}/print`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
rulesetGaps
|
|
3
|
-
} from "./chunk-MNDIED4M.js";
|
|
4
|
-
import {
|
|
5
|
-
makeGitHub
|
|
6
|
-
} from "./chunk-XHHMM5HX.js";
|
|
7
|
-
import "./chunk-LBYOLBW7.js";
|
|
8
|
-
|
|
9
|
-
// src/audits/protection-coverage.ts
|
|
10
|
-
var RENOVATE_WORKFLOW_FILE = "renovate.yml";
|
|
11
|
-
var RENOVATE_STALE_AFTER_DAYS = 3;
|
|
12
|
-
var ACCEPTED_GAPS = [];
|
|
13
|
-
function partitionAcceptedGaps(repo, gaps, now, accepted = ACCEPTED_GAPS) {
|
|
14
|
-
const live = [];
|
|
15
|
-
const acceptedOut = [];
|
|
16
|
-
for (const gap of gaps) {
|
|
17
|
-
const match = accepted.find(
|
|
18
|
-
(a) => a.repo === repo && gap.startsWith(a.detailPrefix) && now < new Date(a.until)
|
|
19
|
-
);
|
|
20
|
-
if (match) acceptedOut.push(`${gap} [accepted until ${match.until}: ${match.reason}]`);
|
|
21
|
-
else live.push(gap);
|
|
22
|
-
}
|
|
23
|
-
return { live, accepted: acceptedOut };
|
|
24
|
-
}
|
|
25
|
-
function secretScanningGaps(repo) {
|
|
26
|
-
const gaps = [];
|
|
27
|
-
if (repo.secretScanning !== "enabled")
|
|
28
|
-
gaps.push(`secret scanning ${repo.secretScanning} (fleet floor is enabled)`);
|
|
29
|
-
if (repo.pushProtection !== "enabled")
|
|
30
|
-
gaps.push(`push protection ${repo.pushProtection} (fleet floor is enabled)`);
|
|
31
|
-
return gaps;
|
|
32
|
-
}
|
|
33
|
-
function renovateGaps(health, now) {
|
|
34
|
-
if (!health.present) return ["no renovate workflow (dependency updates never run here)"];
|
|
35
|
-
if (health.state !== "active")
|
|
36
|
-
return [
|
|
37
|
-
// A disabled workflow REJECTS workflow_dispatch (HTTP 403) — it must be
|
|
38
|
-
// re-enabled first; "just dispatch it" is a dead-end runbook line here.
|
|
39
|
-
`renovate workflow is ${health.state} (re-enable it: \`gh workflow enable renovate.yml -R <repo>\`, then dispatch once)`
|
|
40
|
-
];
|
|
41
|
-
if (health.lastSuccessAt === null)
|
|
42
|
-
return [
|
|
43
|
-
"renovate workflow has never succeeded (dispatch it once; if this gap returns, the schedule never registered \u2014 push a commit touching the workflow file)"
|
|
44
|
-
];
|
|
45
|
-
const succeededAt = Date.parse(health.lastSuccessAt);
|
|
46
|
-
if (Number.isNaN(succeededAt))
|
|
47
|
-
return [`renovate last-success timestamp unreadable: ${health.lastSuccessAt}`];
|
|
48
|
-
const days = (now.getTime() - succeededAt) / 864e5;
|
|
49
|
-
if (days > RENOVATE_STALE_AFTER_DAYS)
|
|
50
|
-
return [`renovate last succeeded ${Math.floor(days)}d ago (cron is twice daily)`];
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
function renovateBlockedGaps(blocked) {
|
|
54
|
-
const orphaned = blocked.filter((b) => b.orphaned);
|
|
55
|
-
if (orphaned.length === 0) return [];
|
|
56
|
-
return [
|
|
57
|
-
`renovate is BLOCKED on ${orphaned.length} branch(es) \u2014 ${orphaned.map((b) => b.branch).join(", ")} \u2014 it runs and succeeds but refuses to update them, so every dependency grouped there is frozen. Usual cause: the branch tip was authored by a retired bot identity, which reads to Renovate as a human edit. Fix: confirm the tip author is a bot, then delete the branch and let Renovate recreate it (\`gh api -X DELETE repos/<repo>/git/refs/heads/<branch>\`).`
|
|
58
|
-
];
|
|
59
|
-
}
|
|
60
|
-
function dashboardVocabularyGaps(dash) {
|
|
61
|
-
if (!dash.present || dash.unknownSections.length === 0) return [];
|
|
62
|
-
return [
|
|
63
|
-
`renovate dashboard has ${dash.unknownSections.length} section(s) this audit does not recognise \u2014 ${dash.unknownSections.join(", ")}. Nothing is necessarily wrong with the repo, but the blocked-branch check reads Renovate's own headings, so drift there means it can no longer be trusted. Fix: reconcile KNOWN_DASHBOARD_SECTIONS in src/github/gh.ts against Renovate's dependency-dashboard.ts.`
|
|
64
|
-
];
|
|
65
|
-
}
|
|
66
|
-
var BLOCKED_STALE_DAYS = 5;
|
|
67
|
-
async function resolveBlockedBranches(repo, dash, deps, now) {
|
|
68
|
-
if (!dash.present) return [];
|
|
69
|
-
const states = [];
|
|
70
|
-
for (const branch of dash.blockedBranches) {
|
|
71
|
-
const tip = await deps.branchTip(repo, branch);
|
|
72
|
-
if (!tip) continue;
|
|
73
|
-
const ageDays = (now.getTime() - new Date(tip.committedAt).getTime()) / 864e5;
|
|
74
|
-
states.push({ branch, orphaned: tip.authorIsMachine || ageDays > BLOCKED_STALE_DAYS });
|
|
75
|
-
}
|
|
76
|
-
return states;
|
|
77
|
-
}
|
|
78
|
-
async function collectProtectionCoverage(org, deps, now = /* @__PURE__ */ new Date(), accepted = ACCEPTED_GAPS) {
|
|
79
|
-
const rows = [];
|
|
80
|
-
for (const r of await deps.listOrgRepos(org)) {
|
|
81
|
-
const repo = `${org}/${r.name}`;
|
|
82
|
-
if (r.archived || r.visibility !== "public") {
|
|
83
|
-
rows.push({
|
|
84
|
-
repo,
|
|
85
|
-
status: "skipped",
|
|
86
|
-
detail: r.archived ? "archived" : `${r.visibility} (rulesets need a paid plan)`
|
|
87
|
-
});
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const gaps = [];
|
|
92
|
-
let coveredDetail = "";
|
|
93
|
-
const rulesets = await deps.listRepoRulesets(repo);
|
|
94
|
-
if (rulesets.length === 0) {
|
|
95
|
-
gaps.push("no repo rulesets at all");
|
|
96
|
-
} else {
|
|
97
|
-
const judged = await Promise.all(
|
|
98
|
-
rulesets.map(async (rs) => {
|
|
99
|
-
const full = await deps.getRuleset(repo, rs.id);
|
|
100
|
-
return { name: rs.name, full, gaps: rulesetGaps(full, null) };
|
|
101
|
-
})
|
|
102
|
-
);
|
|
103
|
-
const covering = judged.find((j) => j.gaps.length === 0);
|
|
104
|
-
if (covering) {
|
|
105
|
-
const ciGated = (covering.full.rules ?? []).some(
|
|
106
|
-
(rule) => rule.type === "required_status_checks"
|
|
107
|
-
);
|
|
108
|
-
coveredDetail = `"${covering.name}"${ciGated ? "" : " \u2014 NO CI gate (refs rules only)"}`;
|
|
109
|
-
} else {
|
|
110
|
-
gaps.push(judged.map((j) => `"${j.name}": ${j.gaps.join("; ")}`).join(" | "));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
gaps.push(...secretScanningGaps(r));
|
|
114
|
-
gaps.push(...renovateGaps(await deps.workflowHealth(repo, RENOVATE_WORKFLOW_FILE), now));
|
|
115
|
-
const dash = await deps.dependencyDashboard(repo);
|
|
116
|
-
gaps.push(...dashboardVocabularyGaps(dash));
|
|
117
|
-
gaps.push(...renovateBlockedGaps(await resolveBlockedBranches(repo, dash, deps, now)));
|
|
118
|
-
const { live, accepted: acked } = partitionAcceptedGaps(repo, gaps, now, accepted);
|
|
119
|
-
if (live.length > 0) {
|
|
120
|
-
rows.push({ repo, status: "gap", detail: [...live, ...acked].join(" | ") });
|
|
121
|
-
} else if (acked.length > 0) {
|
|
122
|
-
rows.push({ repo, status: "skipped", detail: acked.join(" | ") });
|
|
123
|
-
} else {
|
|
124
|
-
rows.push({ repo, status: "covered", detail: coveredDetail });
|
|
125
|
-
}
|
|
126
|
-
} catch (e) {
|
|
127
|
-
rows.push({
|
|
128
|
-
repo,
|
|
129
|
-
status: "gap",
|
|
130
|
-
detail: `probe failed: ${e instanceof Error ? e.message : String(e)}`
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return rows;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// src/cli/commands/protection-audit.ts
|
|
138
|
-
function protectionAuditExitCode(gaps) {
|
|
139
|
-
return gaps > 0 ? 1 : 0;
|
|
140
|
-
}
|
|
141
|
-
async function runProtectionAuditCommand(opts, depsOverride) {
|
|
142
|
-
const org = opts.org?.trim();
|
|
143
|
-
if (!org) {
|
|
144
|
-
return { output: "protection-audit requires --org <org>", code: 2 };
|
|
145
|
-
}
|
|
146
|
-
const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
|
|
147
|
-
if (!depsOverride && !token) {
|
|
148
|
-
return {
|
|
149
|
-
output: "protection-audit skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.",
|
|
150
|
-
code: 0
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
const deps = depsOverride ?? makeGitHub({ token });
|
|
154
|
-
const rows = await collectProtectionCoverage(org, deps);
|
|
155
|
-
const gaps = rows.filter((r) => r.status === "gap");
|
|
156
|
-
const covered = rows.filter((r) => r.status === "covered");
|
|
157
|
-
const skipped = rows.filter((r) => r.status === "skipped");
|
|
158
|
-
const lines = rows.map((r) => `${r.status.toUpperCase().padEnd(7)} ${r.repo} \u2014 ${r.detail}`);
|
|
159
|
-
lines.push(
|
|
160
|
-
`PROTECTION_AUDIT gaps=${gaps.length} covered=${covered.length} skipped=${skipped.length} total=${rows.length}`
|
|
161
|
-
);
|
|
162
|
-
return { output: lines.join("\n"), code: protectionAuditExitCode(gaps.length) };
|
|
163
|
-
}
|
|
164
|
-
export {
|
|
165
|
-
protectionAuditExitCode,
|
|
166
|
-
runProtectionAuditCommand
|
|
167
|
-
};
|
|
168
|
-
//# sourceMappingURL=protection-audit-SJO53MEQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audits/protection-coverage.ts","../src/cli/commands/protection-audit.ts"],"sourcesContent":["import { rulesetGaps, type ExistingRuleset } from \"../github/rulesets.js\";\nimport type { BranchTip, DependencyDashboard, WorkflowHealth } from \"../github/gh.js\";\n\n/**\n * Org-wide protection-coverage sweep (spec:\n * docs/superpowers/specs/2026-08-02-ruleset-self-healing-design.md).\n *\n * The `self-updating` recipe HEALS fleet sites, but it runs only when someone\n * runs it, and only over Airtable-listed sites — which is exactly how the\n * `.github` repo (no Airtable row) sat with zero protection until 2026-08-01.\n * This sweep is the layer that would have caught that on its own: it\n * enumerates the org FROM THE GITHUB API — never a hand-typed or\n * Airtable-scoped list, both of which have already produced false \"all\n * clear\"s — and judges every public repo's protection by SHAPE, not by name,\n * so a differently-named but sound ruleset (reddoor-maintenance's \"Main\n * Protection\") counts.\n *\n * Since 2026-08-02 the same sweep also verifies three more posture surfaces\n * per public repo, because each regresses silently and none has any other\n * watcher:\n * - secret scanning + push protection stay ENABLED (a repo transferred in,\n * or an org-default flip, arrives with them off);\n * - the renovate workflow is registered, active, and has actually RUN\n * recently. GitHub auto-disables schedules after 60 quiet days, and\n * schedule triggers in template-cloned files may never register at all —\n * either way a fully-quiet repo stops receiving updates with zero signal\n * (the 2026-08-02 sweep found two never-run and two 5-days-stale repos on\n * its first pass);\n * - renovate is not only alive but ALLOWED TO ACT — a workflow can run green\n * forever while Renovate has quietly stopped managing a branch, which froze\n * updates on nine repos for a week in August 2026 (see\n * renovateBlockedGaps). Liveness cannot see this; only the outcome can.\n */\nexport type ProtectionCoverageRow = {\n repo: string; // owner/repo\n status: \"covered\" | \"gap\" | \"skipped\";\n /** Human-readable: covering ruleset + whether it gates on CI, the specific\n * gaps, or why the repo was skipped. */\n detail: string;\n};\n\n/** The reads the sweep needs — a subset of the GitHub factory, injected so the\n * sweep is pure + testable (same pattern as collectGitHubSignals). */\nexport type ProtectionCoverageDeps = {\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n listRepoRulesets: (repo: string) => Promise<Array<{ id: number; name: string }>>;\n getRuleset: (repo: string, id: number) => Promise<ExistingRuleset>;\n workflowHealth: (repo: string, filename: string) => Promise<WorkflowHealth>;\n dependencyDashboard: (repo: string) => Promise<DependencyDashboard>;\n branchTip: (repo: string, branch: string) => Promise<BranchTip | null>;\n};\n\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/** Cron is twice daily, so 3 days without a SUCCESSFUL run = 6+ consecutive\n * missed-or-failing runs — dead scheduling, dead credential, or broken\n * config, never jitter. (Success, not run-existence: a revoked App key still\n * creates a fresh failing run every tick.) */\nexport const RENOVATE_STALE_AFTER_DAYS = 3;\n\n/**\n * Operator-accepted gaps — the protection-audit analogue of the cockpit's\n * accepted-Watch acks: a KNOWN condition pending a real decision must not\n * comment the tracking issue every night, or the channel is tuned out by the\n * time a genuinely new gap lands in it. Constraints that keep this honest:\n * every entry is PR-reviewed, names ONE repo + ONE surface (never a blanket\n * mute), states the pending decision, and EXPIRES — past `until` the gap\n * returns on its own, so an acceptance can never quietly become permanent.\n */\nexport type AcceptedGap = {\n repo: string; // owner/repo\n /** Matched as a prefix of the gap detail line — surface-scoped, so an\n * accepted renovate gap never swallows a ruleset or secret-scanning gap. */\n detailPrefix: string;\n reason: string;\n until: string; // ISO date; exclusive — the gap is live again ON this date\n};\n\n// Currently EMPTY — the only two entries (no-renovate on reddoor-maintenance\n// and .github, pending an operator supply-chain call) were retired 2026-08-02\n// when the operator approved planting Renovate on both. Add entries only for\n// a named pending decision, never as a mute.\nexport const ACCEPTED_GAPS: AcceptedGap[] = [];\n\n/** Split gaps into live vs accepted-for-now. Acceptance is per-surface (detail\n * prefix) and expires: an entry past `until` accepts nothing. */\nexport function partitionAcceptedGaps(\n repo: string,\n gaps: string[],\n now: Date,\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): { live: string[]; accepted: string[] } {\n const live: string[] = [];\n const acceptedOut: string[] = [];\n for (const gap of gaps) {\n const match = accepted.find(\n (a) => a.repo === repo && gap.startsWith(a.detailPrefix) && now < new Date(a.until),\n );\n if (match) acceptedOut.push(`${gap} [accepted until ${match.until}: ${match.reason}]`);\n else live.push(gap);\n }\n return { live, accepted: acceptedOut };\n}\n\n/** Public repos only — secret scanning is plan-gated off on private Free\n * repos, and those are skipped before this is consulted. \"unavailable\" means\n * the listing couldn't read security_and_analysis (token lacks admin read),\n * which must read as unverified, not as fine. */\nexport function secretScanningGaps(repo: {\n secretScanning: string;\n pushProtection: string;\n}): string[] {\n const gaps: string[] = [];\n if (repo.secretScanning !== \"enabled\")\n gaps.push(`secret scanning ${repo.secretScanning} (fleet floor is enabled)`);\n if (repo.pushProtection !== \"enabled\")\n gaps.push(`push protection ${repo.pushProtection} (fleet floor is enabled)`);\n return gaps;\n}\n\n/** Renovate liveness verdict — judged on the last SUCCESSFUL run. The cron IS\n * the merge cadence (platform auto-merge is off fleet-wide), so a workflow\n * that isn't succeeding means the repo silently stops updating — the exact\n * class the 2026-07-26 incident review called \"zero-run blindness\". */\nexport function renovateGaps(health: WorkflowHealth, now: Date): string[] {\n if (!health.present) return [\"no renovate workflow (dependency updates never run here)\"];\n if (health.state !== \"active\")\n return [\n // A disabled workflow REJECTS workflow_dispatch (HTTP 403) — it must be\n // re-enabled first; \"just dispatch it\" is a dead-end runbook line here.\n `renovate workflow is ${health.state} (re-enable it: \\`gh workflow enable renovate.yml -R <repo>\\`, then dispatch once)`,\n ];\n if (health.lastSuccessAt === null)\n return [\n \"renovate workflow has never succeeded (dispatch it once; if this gap returns, the schedule never registered — push a commit touching the workflow file)\",\n ];\n const succeededAt = Date.parse(health.lastSuccessAt);\n if (Number.isNaN(succeededAt))\n return [`renovate last-success timestamp unreadable: ${health.lastSuccessAt}`];\n const days = (now.getTime() - succeededAt) / 86_400_000;\n if (days > RENOVATE_STALE_AFTER_DAYS)\n return [`renovate last succeeded ${Math.floor(days)}d ago (cron is twice daily)`];\n return [];\n}\n\n/**\n * Renovate EFFECTIVENESS, which is a different question from liveness.\n *\n * `renovateGaps` above asks \"did the workflow run and succeed?\" — and on\n * 2026-08-03 nine repos answered YES to that while shipping zero updates for a\n * week. The `reddoor-renovate` App migration (2026-08-02) left each repo\n * holding a `renovate/all-minor-patch` branch whose tip was authored by the\n * OLD `renovate-bot` PAT identity. Renovate compares that author to its own,\n * concludes a human edited the branch, files it under \"PR Edited (Blocked) —\n * Renovate will no longer make changes\", and never touches it again. Every run\n * kept succeeding; `@reddoorla/maintenance`, `@sveltejs/kit`, `vite`,\n * `@playwright/test` and `node` were all frozen behind it; CI stayed green and\n * nothing anywhere raised a hand.\n *\n * A liveness probe structurally cannot see this: the workflow is doing its job\n * perfectly, it is simply forbidden to act. So this reads the outcome Renovate\n * itself publishes — the one place it admits to having given up.\n *\n * Absent dashboard is NOT a gap: `dependencyDashboard` is a config a repo may\n * legitimately disable, and the liveness probe already owns the dead-Renovate\n * case.\n *\n * Nor is every blocked branch a gap — see resolveBlockedBranches, which is\n * where the orphan/in-flight distinction is made. Both exclusions serve the\n * same property: this surface reports only what it can stand behind, which is\n * what decides whether an alarm channel still gets read a month from now.\n */\nexport function renovateBlockedGaps(blocked: BlockedBranchState[]): string[] {\n const orphaned = blocked.filter((b) => b.orphaned);\n if (orphaned.length === 0) return [];\n return [\n `renovate is BLOCKED on ${orphaned.length} branch(es) — ` +\n `${orphaned.map((b) => b.branch).join(\", \")} — it runs and succeeds but refuses to ` +\n `update them, so every dependency grouped there is frozen. Usual cause: the branch ` +\n `tip was authored by a retired bot identity, which reads to Renovate as a human ` +\n `edit. Fix: confirm the tip author is a bot, then delete the branch and let ` +\n `Renovate recreate it (\\`gh api -X DELETE repos/<repo>/git/refs/heads/<branch>\\`).`,\n ];\n}\n\n/**\n * A dashboard whose section vocabulary we do not share. Reported separately\n * from blocked branches because it is a different claim: not \"Renovate is\n * stuck\" but \"this audit can no longer tell whether Renovate is stuck.\" The\n * blocked heading was renamed once already (Edited/Blocked -> PR Edited\n * (Blocked) in Renovate 43), and the fleet inherits its Renovate major from\n * whatever renovatebot/github-action bakes in — so the next rename arrives via\n * an ordinary dependency PR that Renovate merges in-run, with nobody reading\n * dashboard wording. Without this, that rename would flip every repo to\n * covered on the same night and the surface would go permanently blind.\n */\nexport function dashboardVocabularyGaps(dash: DependencyDashboard): string[] {\n if (!dash.present || dash.unknownSections.length === 0) return [];\n return [\n `renovate dashboard has ${dash.unknownSections.length} section(s) this audit does ` +\n `not recognise — ${dash.unknownSections.join(\", \")}. Nothing is necessarily wrong ` +\n `with the repo, but the blocked-branch check reads Renovate's own headings, so ` +\n `drift there means it can no longer be trusted. Fix: reconcile ` +\n `KNOWN_DASHBOARD_SECTIONS in src/github/gh.ts against Renovate's ` +\n `dependency-dashboard.ts.`,\n ];\n}\n\n/** How long a human-owned blocked branch may sit before it stops reading as\n * in-flight work. Sized to clear a weekend plus slack: the fleet's own\n * human-edited Renovate PRs merge in minutes, while the branches that froze\n * nine repos sat for a week. */\nexport const BLOCKED_STALE_DAYS = 5;\n\nexport type BlockedBranchState = { branch: string; orphaned: boolean };\n\n/**\n * Decide which blocked branches are actually a posture problem.\n *\n * `PR Edited (Blocked)` is not by itself a fault — it is also Renovate's\n * designed way of saying \"a human owns this branch now\", and pushing a commit\n * onto an open Renovate PR is routine fleet practice (16 such PRs across 14\n * repos on a single day). Alarming on those would fire the nightly issue for\n * perfectly healthy in-flight work, and would attach a remediation that says\n * to delete the branch — i.e. the human's commits.\n *\n * A branch counts as ORPHANED, and therefore a gap, when either:\n * - its tip was authored by a machine, so no human owns it and nothing will\n * ever clear it on its own; or\n * - it has sat untouched past BLOCKED_STALE_DAYS, whoever authored it —\n * because Renovate updates a branch it is managing, so tip age IS time\n * spent frozen.\n *\n * The authorship test is the fast path and the staleness test is the backstop:\n * if machine-identity detection ever drifts, detection degrades to slower, not\n * to blind. A branch that no longer exists is not a gap — that is a dashboard\n * Renovate has not rewritten yet.\n */\nexport async function resolveBlockedBranches(\n repo: string,\n dash: DependencyDashboard,\n deps: Pick<ProtectionCoverageDeps, \"branchTip\">,\n now: Date,\n): Promise<BlockedBranchState[]> {\n if (!dash.present) return [];\n const states: BlockedBranchState[] = [];\n for (const branch of dash.blockedBranches) {\n const tip = await deps.branchTip(repo, branch);\n if (!tip) continue;\n const ageDays = (now.getTime() - new Date(tip.committedAt).getTime()) / 86_400_000;\n states.push({ branch, orphaned: tip.authorIsMachine || ageDays > BLOCKED_STALE_DAYS });\n }\n return states;\n}\n\n/**\n * One row per org repo. Coverage = SOME repo-sourced ruleset with zero\n * stage-1 gaps (active, empty bypass, default branch covered, deletion +\n * non_fast_forward + pull_request) AND secret scanning enabled AND a live\n * renovate workflow. Whether a covering ruleset also gates on CI is reported\n * as detail, not judged: each repo's required context differs (and\n * reddoor-maintenance deliberately has none pending release-path review), so\n * the CI-gate invariant belongs to the per-site heal, which has the evidence\n * to require the RIGHT context safely.\n *\n * Private/archived repos are skipped (rulesets on private repos need a paid\n * plan; archived repos are read-only). A repo whose probe THROWS is a `gap`,\n * not a skip — \"couldn't verify\" reading as \"fine\" is the silent-green\n * failure mode this sweep exists to kill, and the tracking issue auto-closes\n * on the next clean pass, so a transient API blip costs one issue-comment.\n */\nexport async function collectProtectionCoverage(\n org: string,\n deps: ProtectionCoverageDeps,\n now: Date = new Date(),\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): Promise<ProtectionCoverageRow[]> {\n const rows: ProtectionCoverageRow[] = [];\n for (const r of await deps.listOrgRepos(org)) {\n const repo = `${org}/${r.name}`;\n if (r.archived || r.visibility !== \"public\") {\n rows.push({\n repo,\n status: \"skipped\",\n detail: r.archived ? \"archived\" : `${r.visibility} (rulesets need a paid plan)`,\n });\n continue;\n }\n try {\n const gaps: string[] = [];\n let coveredDetail = \"\";\n const rulesets = await deps.listRepoRulesets(repo);\n if (rulesets.length === 0) {\n gaps.push(\"no repo rulesets at all\");\n } else {\n const judged = await Promise.all(\n rulesets.map(async (rs) => {\n const full = await deps.getRuleset(repo, rs.id);\n return { name: rs.name, full, gaps: rulesetGaps(full, null) };\n }),\n );\n const covering = judged.find((j) => j.gaps.length === 0);\n if (covering) {\n const ciGated = (covering.full.rules ?? []).some(\n (rule) => rule.type === \"required_status_checks\",\n );\n coveredDetail = `\"${covering.name}\"${ciGated ? \"\" : \" — NO CI gate (refs rules only)\"}`;\n } else {\n gaps.push(judged.map((j) => `\"${j.name}\": ${j.gaps.join(\"; \")}`).join(\" | \"));\n }\n }\n gaps.push(...secretScanningGaps(r));\n gaps.push(...renovateGaps(await deps.workflowHealth(repo, RENOVATE_WORKFLOW_FILE), now));\n const dash = await deps.dependencyDashboard(repo);\n gaps.push(...dashboardVocabularyGaps(dash));\n gaps.push(...renovateBlockedGaps(await resolveBlockedBranches(repo, dash, deps, now)));\n const { live, accepted: acked } = partitionAcceptedGaps(repo, gaps, now, accepted);\n if (live.length > 0) {\n // Accepted annotations still ride along for context, but only LIVE\n // gaps make the row a gap (and thus the sweep exit 1).\n rows.push({ repo, status: \"gap\", detail: [...live, ...acked].join(\" | \") });\n } else if (acked.length > 0) {\n // Judged, found wanting, deliberately acked — reported as skipped\n // (visible every night with the reason + expiry), never as covered.\n rows.push({ repo, status: \"skipped\", detail: acked.join(\" | \") });\n } else {\n rows.push({ repo, status: \"covered\", detail: coveredDetail });\n }\n } catch (e) {\n rows.push({\n repo,\n status: \"gap\",\n detail: `probe failed: ${e instanceof Error ? e.message : String(e)}`,\n });\n }\n }\n return rows;\n}\n","import {\n collectProtectionCoverage,\n type ProtectionCoverageDeps,\n} from \"../../audits/protection-coverage.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\n\n/** Exit 1 on ANY gap: unlike the fleet write sweeps (majority-rule), a single\n * unprotected public repo is a real hole, not a flake to tolerate. */\nexport function protectionAuditExitCode(gaps: number): number {\n return gaps > 0 ? 1 : 0;\n}\n\n/** `protection-audit --org <org>`: verify every public repo in the org carries\n * a sound branch ruleset, live secret scanning + push protection, and a\n * renovate workflow that actually runs (see collectProtectionCoverage for the\n * verdicts). Emits one line per repo + a machine-readable PROTECTION_AUDIT\n * summary the nightly workflow gates its tracking issue on. A missing fleet\n * token is a clean skip (local runs), matching github-signals. */\nexport async function runProtectionAuditCommand(\n opts: { org?: string | undefined },\n // Injected in tests; defaults to the real gh-CLI-backed factory.\n depsOverride?: ProtectionCoverageDeps,\n): Promise<{ output: string; code: number }> {\n const org = opts.org?.trim();\n if (!org) {\n return { output: \"protection-audit requires --org <org>\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!depsOverride && !token) {\n return {\n output: \"protection-audit skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.\",\n code: 0,\n };\n }\n const deps = depsOverride ?? makeGitHub({ token: token! });\n\n const rows = await collectProtectionCoverage(org, deps);\n const gaps = rows.filter((r) => r.status === \"gap\");\n const covered = rows.filter((r) => r.status === \"covered\");\n const skipped = rows.filter((r) => r.status === \"skipped\");\n\n const lines = rows.map((r) => `${r.status.toUpperCase().padEnd(7)} ${r.repo} — ${r.detail}`);\n lines.push(\n `PROTECTION_AUDIT gaps=${gaps.length} covered=${covered.length} skipped=${skipped.length} total=${rows.length}`,\n );\n return { output: lines.join(\"\\n\"), code: protectionAuditExitCode(gaps.length) };\n}\n"],"mappings":"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","names":[]}
|