@metabase/cli 0.5.7 → 0.5.8
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/dist/{add-collection-BbPkd9Hj.mjs → add-collection-DIrpeq9O.mjs} +3 -3
- package/dist/{alert-DSql1Ep7.mjs → alert-DuUreVMq.mjs} +7 -7
- package/dist/{alerts-DuBBNAlw.mjs → alerts-C7vJ-NAI.mjs} +6 -6
- package/dist/api-key-Cia6SHQ9.mjs +13 -0
- package/dist/{append-DsVKGdZJ.mjs → append-v3fakJOd.mjs} +4 -4
- package/dist/{archive-DD__EWKz.mjs → archive-5pjvTser.mjs} +3 -3
- package/dist/{archive-BV6Z3YJA.mjs → archive-BEhhmkTO.mjs} +3 -3
- package/dist/{archive-Ck_Xg7ns.mjs → archive-BrGj2yoP.mjs} +3 -3
- package/dist/{archive-1VQCMgaz.mjs → archive-Bt8fwInI.mjs} +4 -4
- package/dist/{archive-DqJwr9vQ.mjs → archive-CmhV_hlP.mjs} +3 -3
- package/dist/{archive-DyMGCNDz.mjs → archive-CuygjxgQ.mjs} +3 -3
- package/dist/{archive-BCmfehL3.mjs → archive-Cy6RUJaD.mjs} +3 -3
- package/dist/{archive-CXMG-fgO.mjs → archive-CyhH-395.mjs} +3 -3
- package/dist/{archive-CXXxSfha.mjs → archive-D299FxXt.mjs} +3 -3
- package/dist/{archive-DXz6AUiA.mjs → archive-DdPaXaJo.mjs} +4 -4
- package/dist/{archive-Dvh9rizn.mjs → archive-DwVbn3FG.mjs} +3 -3
- package/dist/{archive-pSV2qSiA.mjs → archive-TBPRGm7s.mjs} +3 -3
- package/dist/{auth-J8JdlqFn.mjs → auth-B63Hfuwg.mjs} +5 -5
- package/dist/{branches-CPQ3HP-R.mjs → branches-BM0L124r.mjs} +3 -3
- package/dist/{breaking-D6ICAVOR.mjs → breaking-VMUrgVVE.mjs} +5 -5
- package/dist/{broken-BczVkbIB.mjs → broken-BbpNq5WG.mjs} +6 -6
- package/dist/{bulk-discard-values-g46lzzus.mjs → bulk-discard-values-Cohox-Zb.mjs} +5 -5
- package/dist/{bulk-edit-AOFHuhfZ.mjs → bulk-edit-B9-XH592.mjs} +3 -3
- package/dist/{bulk-rescan-values-5Di3olNO.mjs → bulk-rescan-values-CsWKirZw.mjs} +4 -4
- package/dist/{bulk-sync-schema-CFC3koGc.mjs → bulk-sync-schema-CorpcAjz.mjs} +4 -4
- package/dist/{cancel-BGYUVj8X.mjs → cancel-iGUVm1h2.mjs} +3 -3
- package/dist/{cancel-task-upC5n6uL.mjs → cancel-task-DWiicb_t.mjs} +2 -2
- package/dist/card-C-KsmoJW.mjs +25 -0
- package/dist/{cards-Bi0gfBth.mjs → cards-D6jWz0N9.mjs} +4 -4
- package/dist/ci-BQKTyiRi.mjs +9 -0
- package/dist/cli.mjs +68 -43
- package/dist/{client-BLoUVLvN.mjs → client-Ba61gEcn.mjs} +6 -6
- package/dist/collection-DsNRlVcN.mjs +21 -0
- package/dist/{collection-namespace-C0JcSEwN.mjs → collection-namespace-D0_-6eWp.mjs} +1 -1
- package/dist/{content-translation-BpLzaU0B.mjs → content-translation-B57aHSuc.mjs} +3 -3
- package/dist/{copy-UsVSYgy4.mjs → copy-BeD_fk5J.mjs} +4 -4
- package/dist/{copy-f95AEumd.mjs → copy-D-VIVVB1.mjs} +4 -4
- package/dist/{copy-flags-DZ5Cy-Gk.mjs → copy-flags-BcW9weVX.mjs} +1 -1
- package/dist/{create-CuNXv1Ep.mjs → create-B--aPIbp.mjs} +4 -4
- package/dist/{create-GkTAKk5R.mjs → create-BURh8kHp.mjs} +3 -3
- package/dist/{create-x7HAP-Ad.mjs → create-BlmRvuQg.mjs} +3 -3
- package/dist/{create-BD6VIGWQ2.mjs → create-Bw9DAq_x2.mjs} +5 -5
- package/dist/{create-Di1QVcNp.mjs → create-C6c-clLK.mjs} +3 -3
- package/dist/{create-D5LhDFOo.mjs → create-CGiSOtIR.mjs} +3 -3
- package/dist/{create-CO18QOFb.mjs → create-CP9lrYE4.mjs} +2 -2
- package/dist/{create-CVh6ggJb.mjs → create-D-Eu1nBe.mjs} +2 -2
- package/dist/{create-DBX-uy9W.mjs → create-D44fg_iT.mjs} +2 -2
- package/dist/{create-CY9LOAXi.mjs → create-DF7nC9qW.mjs} +2 -2
- package/dist/{create-u0CTmvuf.mjs → create-DMtlDzrE.mjs} +3 -3
- package/dist/{create-D5kh4vAL.mjs → create-DQOapeeI.mjs} +3 -3
- package/dist/{create-CeczpJ0L.mjs → create-DcDyXL4g.mjs} +2 -2
- package/dist/{create-k4mrdwwl.mjs → create-Dm8BvbsU.mjs} +2 -2
- package/dist/{create-DLFkb3OR.mjs → create-DybrZHV-.mjs} +2 -2
- package/dist/{create-BbC_R_Oz.mjs → create-HNchuWpj.mjs} +3 -3
- package/dist/{create-7K2NXL-M.mjs → create-MmZVmRsH.mjs} +2 -2
- package/dist/{create-BNcuZIPD.mjs → create-_wex54m1.mjs} +2 -2
- package/dist/{create-branch-Bo0AjZtH.mjs → create-branch-C8arQT4M.mjs} +2 -2
- package/dist/{create-lzRFz7kJ.mjs → create-lHqpAdxg.mjs} +3 -3
- package/dist/{create-7ncgawHS.mjs → create-zALsdMhF.mjs} +2 -2
- package/dist/{csv-DwFtBGrk.mjs → csv-CFabsH6l.mjs} +4 -4
- package/dist/{current-task-BJtRUPla.mjs → current-task-GG6JqfGp.mjs} +2 -2
- package/dist/dashboard-BRQtw_1e.mjs +35 -0
- package/dist/data-action-Bm6QMyV0.mjs +24 -0
- package/dist/{data-app-DO2JDssf.mjs → data-app-DtW2i2SD.mjs} +10 -10
- package/dist/{data-app-project-DUCJcvfJ.mjs → data-app-project-CHYcJRrU.mjs} +1 -1
- package/dist/{db-N75yLlEU.mjs → db-DSj2I1X9.mjs} +8 -8
- package/dist/{delete-DxiCwXgz.mjs → delete-AF64p0xG.mjs} +4 -4
- package/dist/{delete-vwfsnQPy.mjs → delete-C6N_Aj78.mjs} +4 -4
- package/dist/{delete-Cl4U9VSc.mjs → delete-ClT1ROrM.mjs} +3 -3
- package/dist/{delete-8OdbYsJ5.mjs → delete-CnGb4QIm.mjs} +4 -4
- package/dist/{delete-l2szNHB_.mjs → delete-CsgkbmBy.mjs} +4 -4
- package/dist/{delete-CnASt2yl.mjs → delete-Cx75BsN6.mjs} +4 -4
- package/dist/{delete-C4xxlLHk.mjs → delete-CxL5okLN.mjs} +4 -4
- package/dist/{delete-DeSsSMfX.mjs → delete-D_l7FRE6.mjs} +4 -4
- package/dist/{delete-BdOdU-pY.mjs → delete-Dkwk2mZH.mjs} +4 -4
- package/dist/{delete-CjaFm4k-.mjs → delete-YKX2HilI.mjs} +4 -4
- package/dist/{delete-runtime-DOOy3gzH.mjs → delete-runtime-BUbOQx5I.mjs} +1 -1
- package/dist/{delete-table-BUF-8jMm.mjs → delete-table-CubRk-5H.mjs} +4 -4
- package/dist/{dependencies-DZX8NlPW.mjs → dependencies-W2dxsCpF.mjs} +5 -5
- package/dist/dependency-B0KmVz2u.mjs +15 -0
- package/dist/{dependency-DcM96XJ-.mjs → dependency-BPCRTBKd.mjs} +1 -1
- package/dist/{dependents-BqO3l5bu.mjs → dependents-B4V8pDYq.mjs} +6 -6
- package/dist/{dirty-ovaaa9Mz.mjs → dirty-DKj4ScBN.mjs} +3 -3
- package/dist/{discard-values-C0hxOxST.mjs → discard-values-5l91z3Dy.mjs} +5 -5
- package/dist/document--zYZP-u3.mjs +20 -0
- package/dist/{download-TKkYyPGz.mjs → download-DRxR3yGi.mjs} +2 -2
- package/dist/{eid-BMEaAUf7.mjs → eid-B6JYogNC.mjs} +2 -2
- package/dist/{entry-flags-B1iNsTLZ.mjs → entry-flags-6Y0fdYbG.mjs} +1 -1
- package/dist/{events-DawQN6kU.mjs → events-CE80dTib.mjs} +4 -4
- package/dist/{execute-YbkICF1K.mjs → execute-C9foK2IU.mjs} +3 -3
- package/dist/{export-X-6wfnwT.mjs → export-B8QyoPWP.mjs} +3 -3
- package/dist/{export-flags-iXfEILf7.mjs → export-flags-DvdxpRMt.mjs} +1 -1
- package/dist/{export-preflight-B8v9LJNm.mjs → export-preflight-BQbwxyoJ.mjs} +2 -2
- package/dist/field-Bk09crKC.mjs +21 -0
- package/dist/{fields-BhXMw2Tn.mjs → fields-TE3PnmwW.mjs} +4 -4
- package/dist/{filter-flags-ezqqsOQL.mjs → filter-flags-DAUmdUwa.mjs} +1 -1
- package/dist/{fks-8cVnZFmC.mjs → fks-B41Re5dt.mjs} +5 -5
- package/dist/{get-C7z5ZvgH.mjs → get-3XwqrjxC.mjs} +3 -3
- package/dist/{get-CZ1rSEC1.mjs → get-BAGQ-bvt.mjs} +3 -3
- package/dist/{get-4f27Aqvg.mjs → get-BJgBsiEK.mjs} +2 -2
- package/dist/{get-Dp1ujLbV.mjs → get-BcWr6U0n.mjs} +2 -2
- package/dist/{get-I1_CB82F.mjs → get-BrhIHZYg.mjs} +3 -3
- package/dist/{get-CM_iqCVJ.mjs → get-BscyItLF.mjs} +3 -3
- package/dist/{get-C83Nptcn.mjs → get-CCTRO18v.mjs} +4 -4
- package/dist/{get-B6uimv77.mjs → get-CU6AWbqL.mjs} +3 -3
- package/dist/{get-Dvt9zn0x.mjs → get-CefCrGWy.mjs} +5 -5
- package/dist/{get-zwsgING1.mjs → get-Cjk-A9dQ.mjs} +3 -3
- package/dist/{get-DNbe2MG3.mjs → get-Ck0cRNyr.mjs} +4 -4
- package/dist/{get-Bo96ebpj.mjs → get-CtRJUybv.mjs} +3 -3
- package/dist/{get-BF3Uo0Tt.mjs → get-DG5fPl-S.mjs} +4 -4
- package/dist/{get-B2ml9ucl.mjs → get-DJeeXbwj.mjs} +3 -3
- package/dist/{get-BbL5r1fz.mjs → get-DbZDp2ve.mjs} +3 -3
- package/dist/{get-CeKWEfkY.mjs → get-DeiC05Lm.mjs} +3 -3
- package/dist/{get-Dxtlj2My.mjs → get-DguRhloC.mjs} +2 -2
- package/dist/{get-CDnlRGML.mjs → get-DvPgnqAA.mjs} +2 -2
- package/dist/{get-Bw4pTxpu.mjs → get-PLIbg3Vy.mjs} +3 -3
- package/dist/{get-edf6ZezS.mjs → get-m-vpr7GN.mjs} +3 -3
- package/dist/{get-run-BIsvXGiW.mjs → get-run-DnKCnvUa.mjs} +4 -4
- package/dist/{get-BtBc5Abl.mjs → get-sjhoDJyB.mjs} +5 -5
- package/dist/{get-Cpx-XbMF.mjs → get-uWk0JKvF.mjs} +4 -4
- package/dist/git-sync-DMyH3Y85.mjs +29 -0
- package/dist/glossary-CcRGJaVi.mjs +14 -0
- package/dist/{graph-r_Ny78Et.mjs → graph-B2YkV2oX.mjs} +4 -4
- package/dist/{group--6adAsqG.mjs → group-D1nGaEXW.mjs} +1 -1
- package/dist/{has-remote-changes-B_i31H2V.mjs → has-remote-changes-ChMSjhmC.mjs} +2 -2
- package/dist/{import-po_7IOPt.mjs → import-CNpN9iS0.mjs} +3 -3
- package/dist/{is-dirty-PBHnBaT_.mjs → is-dirty-Bud2wuxQ.mjs} +2 -2
- package/dist/{items-DpYoeJnW.mjs → items-B1DXzdwg.mjs} +3 -3
- package/dist/{library-Bx-5aHg0.mjs → library-8G0s5VLO.mjs} +5 -5
- package/dist/{list-T__r3VMU.mjs → list-0aX638ov.mjs} +5 -5
- package/dist/{list-C12p3rEc.mjs → list-B2PbnXp4.mjs} +4 -4
- package/dist/{list-DZy5euhj.mjs → list-BJasizCe.mjs} +3 -3
- package/dist/{list-BALDFgbl.mjs → list-BSvQ0AT_.mjs} +3 -3
- package/dist/{list-C1Wh_Slv.mjs → list-BVz0N0g_.mjs} +3 -3
- package/dist/{list-CrWXMfr6.mjs → list-BeaA7gQ7.mjs} +3 -3
- package/dist/{list-CBwWhmqe.mjs → list-CAAvhl9h.mjs} +3 -3
- package/dist/{list-DS9fMuno.mjs → list-CAZHyhd5.mjs} +3 -3
- package/dist/{list-3LnxA7HU.mjs → list-CO62N1eD.mjs} +3 -3
- package/dist/{list-BOyLrnUG.mjs → list-CPwy-AEn.mjs} +3 -3
- package/dist/{list-DyQ2lSVl.mjs → list-CcfGHN19.mjs} +3 -3
- package/dist/{list-Cr_sPpp-.mjs → list-Ckqu41Zx.mjs} +3 -3
- package/dist/{list-DKV9DtGa.mjs → list-D1WO7lxP.mjs} +4 -4
- package/dist/{list-X8eebbXY.mjs → list-DAdWd8gn.mjs} +3 -3
- package/dist/{list-DVqD1U1l.mjs → list-DX5SmBCQ.mjs} +3 -3
- package/dist/{list-cK0UL1ZA.mjs → list-DZLGnsAK.mjs} +3 -3
- package/dist/{list-DceMKQ6a.mjs → list-DjfY80vj.mjs} +3 -3
- package/dist/{list-BGwYQo6I.mjs → list-Dk3upD11.mjs} +5 -5
- package/dist/{list-B8RvfRvk.mjs → list-DpyIV41y.mjs} +6 -6
- package/dist/{list-xbkz1qBV.mjs → list-JqIFAXpw.mjs} +3 -3
- package/dist/{list-Y5wS6EaZ.mjs → list-mRSKPKoO.mjs} +3 -3
- package/dist/{list-jEl4fNSi.mjs → list-tHzgEkzv.mjs} +5 -5
- package/dist/{list-DxdHEB5R.mjs → list-z4iqffgl.mjs} +4 -4
- package/dist/{login-P2L_8iQN.mjs → login-muOdP15h.mjs} +5 -5
- package/dist/{logout-D-WoK1AI.mjs → logout-Z0_gSSM4.mjs} +2 -2
- package/dist/{measure-D2r-3q-X.mjs → measure-CTF4qqiE.mjs} +6 -6
- package/dist/{notification-BEJaEZS2.mjs → notification-Bj45MgWm.mjs} +1 -1
- package/dist/{parameter-values-BD9l9Hyp.mjs → parameter-values-to5u_-2n.mjs} +3 -3
- package/dist/{parse-id-EXZan40V.mjs → parse-id-CbF7zxGv.mjs} +1 -1
- package/dist/{path-CRMn9iZY.mjs → path-B-0S-9i2.mjs} +4 -4
- package/dist/{poll-DqqOqx78.mjs → poll-zB3cX0Kq.mjs} +1 -1
- package/dist/{preflight-4iy2qzsh.mjs → preflight-Dgj2yyY_.mjs} +1 -1
- package/dist/process-ByKCTa6A.mjs +156 -0
- package/dist/{publish-D0z76x-b.mjs → publish-tKh9tJeh.mjs} +3 -3
- package/dist/{pulse-CPSpum5U.mjs → pulse-DhddeMMG.mjs} +1 -1
- package/dist/{query-zFuvlOS0.mjs → query-B4xK9j0u.mjs} +4 -4
- package/dist/{query-CUVLtPE_.mjs → query-o1Z2dgtw.mjs} +4 -4
- package/dist/{remapping-Dhb722en.mjs → remapping-DT305P-k.mjs} +3 -3
- package/dist/{remove-collection-DZXgxdKX.mjs → remove-collection-Bsme1OBA.mjs} +3 -3
- package/dist/{render-oEZzsoFQ.mjs → render-Df56xOPJ.mjs} +1 -1
- package/dist/{replace-7dPKXgcC.mjs → replace-C3PQwvfl.mjs} +4 -4
- package/dist/{requirement-check-DAWBg7Q9.mjs → requirement-check-qqRkRGcN.mjs} +2 -2
- package/dist/{rescan-values-DSr_ZvG_.mjs → rescan-values-C0P0upEB.mjs} +4 -4
- package/dist/{rescan-values-CLrviE6Q.mjs → rescan-values-CGN4bjyB.mjs} +3 -3
- package/dist/{run-B0xlfNxz.mjs → run-BaEBHum8.mjs} +4 -4
- package/dist/run-C_IZfmQ7.mjs +1055 -0
- package/dist/{run-ZSHtfyJ5.mjs → run-ZRY5wy4x.mjs} +6 -6
- package/dist/{run-C-7Jqr7g.mjs → run-bVD66LrU.mjs} +3 -3
- package/dist/{runs-DhTxL1ZX.mjs → runs-DWm1pIdP.mjs} +5 -5
- package/dist/{runtime-LdcG5yv4.mjs → runtime-Dd2uBKqN.mjs} +3 -3
- package/dist/{scaffold-DjXyOYBc.mjs → scaffold-BnT99dhY.mjs} +4 -4
- package/dist/{schema-olxaMFgn.mjs → schema-DoELV3FT.mjs} +2 -2
- package/dist/{schema-tables-C8PvLCZ3.mjs → schema-tables-DGyHJnJX.mjs} +5 -5
- package/dist/{schemas-CHXrSjAS.mjs → schemas-7w9MPZcZ.mjs} +4 -4
- package/dist/{search-BUoRSx00.mjs → search-BZh0BwZ9.mjs} +4 -4
- package/dist/{search-xkiNHiwe.mjs → search-CyyySVTz.mjs} +4 -4
- package/dist/{segment-C6A1dSBA.mjs → segment--JCnZCgL.mjs} +6 -6
- package/dist/{send-CaDx_a6O.mjs → send-c4FpapMf.mjs} +3 -3
- package/dist/{set-gJ6v5NIe.mjs → set-_puxfyaP.mjs} +2 -2
- package/dist/{set-active-Db1w9BRn.mjs → set-active-BexM75_s.mjs} +2 -2
- package/dist/{set-bundle-Dlk0ZEHn.mjs → set-bundle-B-qAvkDo.mjs} +3 -3
- package/dist/{set-data-actions-BPnO0ZDb.mjs → set-data-actions-BnrX6ARh.mjs} +3 -3
- package/dist/{set-enabled-DmC4OaLp.mjs → set-enabled-Np4xqFA_.mjs} +2 -2
- package/dist/{set-sensitivity-BD1R9re1.mjs → set-sensitivity-DDzrkjdv.mjs} +3 -3
- package/dist/{setting-C2jQD_g8.mjs → setting-BGK1uRxm.mjs} +4 -4
- package/dist/{setup-QmD-t1pE.mjs → setup-BCliYRAt.mjs} +2 -2
- package/dist/{skill-list-BoJITnw8.mjs → skill-list-4LagdTq5.mjs} +1 -1
- package/dist/{skills-BAS2gDjf.mjs → skills-CoXIflBw.mjs} +3 -3
- package/dist/{skills-SJUpkpK0.mjs → skills-PULUh2VV.mjs} +4 -39
- package/dist/snippet-Czzma-M2.mjs +19 -0
- package/dist/{stash-DqmWy3Sh.mjs → stash-d3FtnE2F.mjs} +3 -3
- package/dist/{status-D53dMSSs.mjs → status-BJTdBEN4.mjs} +3 -3
- package/dist/{status-eg3_xFyQ.mjs → status-CJLd6Uk6.mjs} +2 -2
- package/dist/{subscription-D9e9tr6s.mjs → subscription-Dpq45fMP.mjs} +6 -6
- package/dist/{subscriptions-Bqqlqf5T.mjs → subscriptions-Cu4km2CN.mjs} +6 -6
- package/dist/{summary-BpTu2cty.mjs → summary-3Av5251t.mjs} +3 -3
- package/dist/{sync-schema-DaTcqvaG.mjs → sync-schema-BNHt8S3P.mjs} +4 -4
- package/dist/{sync-schema-wJ7q9gAE.mjs → sync-schema-BYJuUBsG.mjs} +4 -4
- package/dist/table-CwfF6ZTZ.mjs +26 -0
- package/dist/{table-BNA9Or05.mjs → table-DFbkLSgK.mjs} +1 -1
- package/dist/{table-selector-flags-BpFFoSaT.mjs → table-selector-flags-BiA8p56s.mjs} +2 -2
- package/dist/timeline-C4WCMzDX.mjs +21 -0
- package/dist/{timeline-event-feyA3Udc.mjs → timeline-event-BLBqSAjH.mjs} +6 -6
- package/dist/{transform-DKPtUV54.mjs → transform-CKeLIqoc.mjs} +1 -1
- package/dist/transform-job-SQ57QOlQ.mjs +22 -0
- package/dist/transform-jxF8cjNA.mjs +28 -0
- package/dist/{transform-tag-CKSAwl4d.mjs → transform-tag-CETHXuAi.mjs} +5 -5
- package/dist/{transform-test-C49ZTK7M.mjs → transform-test-D-e9oi6r.mjs} +1 -1
- package/dist/{transform-test-Bexa_K89.mjs → transform-test-D6QIyu8C.mjs} +5 -1
- package/dist/transform-test-MlVjTlKo.mjs +16 -0
- package/dist/{transforms-VsNAUcKg.mjs → transforms-CstSQgQ-.mjs} +5 -5
- package/dist/{transport-5CHP_tMs.mjs → transport-D74axlBJ.mjs} +3 -3
- package/dist/{tree-CPjGQ2wm.mjs → tree-BDhm4E_W.mjs} +2 -2
- package/dist/{typed-schema-W4OW1Edq.mjs → typed-schema-BZkv_z2f.mjs} +3 -3
- package/dist/{unpublish-PuVviojR.mjs → unpublish-BNBSxZFl.mjs} +3 -3
- package/dist/{unreferenced-BT9gANlE.mjs → unreferenced-B2nDcQ7m.mjs} +5 -5
- package/dist/{update-DJBofThn.mjs → update-B6xWuJqk.mjs} +3 -3
- package/dist/{update-TV4PBAZv.mjs → update-B9iHHoJB.mjs} +4 -4
- package/dist/{update-Da3-3dUO.mjs → update-BA0NxInb.mjs} +4 -4
- package/dist/{update-n1X8JM4b.mjs → update-BGFGng3a.mjs} +4 -4
- package/dist/{update-EXLe8XCO.mjs → update-BMHPp3nt.mjs} +3 -3
- package/dist/{update-CNIi5f88.mjs → update-BS-E7QLh.mjs} +4 -4
- package/dist/{update-DZs7eBGa.mjs → update-BenGrugz.mjs} +4 -4
- package/dist/{update-tlpf9Nd7.mjs → update-BkFy2BV_.mjs} +4 -4
- package/dist/{update--2TTQzzF.mjs → update-ByBqo1if.mjs} +4 -4
- package/dist/{update-DeLyAU3L.mjs → update-CFLJWHOT.mjs} +3 -3
- package/dist/{update-Xc-hfOcM.mjs → update-CWzmrGS_.mjs} +3 -3
- package/dist/{update-ircRP-Ga.mjs → update-Cpd1tKqr.mjs} +3 -3
- package/dist/{update-Dmhrlzst.mjs → update-DKcPzoG9.mjs} +5 -5
- package/dist/{update-lrnQ5RIy.mjs → update-DRw_610p.mjs} +4 -4
- package/dist/{update-BGXD76ks.mjs → update-DT8KwBxO.mjs} +4 -4
- package/dist/{update-CGYUDxv3.mjs → update-DU3jpGps.mjs} +3 -3
- package/dist/{update-qNsPwe0E.mjs → update-DrfAOlkf.mjs} +2 -2
- package/dist/{update-Bo6KGX9D2.mjs → update-DrvDC_0m2.mjs} +3 -3
- package/dist/{update-Dp3AUNWA.mjs → update-DxLypq9G.mjs} +6 -6
- package/dist/{update-dashcard-3jW0jYXV.mjs → update-dashcard-BU3Wqcnf.mjs} +3 -3
- package/dist/{update-CBC8lAqs.mjs → update-pzu78yZB.mjs} +3 -3
- package/dist/{upgrade-C08e6bwK.mjs → upgrade-Cq89D4Oe.mjs} +3 -3
- package/dist/{upload-B3pz8QNS.mjs → upload-CmwO6anj.mjs} +1 -1
- package/dist/upload-DbDFl9qT.mjs +13 -0
- package/dist/{upload-Cazgw9jX.mjs → upload-INQHve86.mjs} +3 -3
- package/dist/{uuid-B1PCdTpU.mjs → uuid-BNJKaQGB.mjs} +2 -2
- package/dist/{validate-query-Bf5FhhMI.mjs → validate-query-CQ4UPpZX.mjs} +1 -1
- package/dist/{values-Ym6iwLLB.mjs → values-BVtqwRlR.mjs} +3 -3
- package/dist/{verify-CnC3Oemx.mjs → verify-BSlj7S_E.mjs} +3 -3
- package/dist/{verify-CIxbC-Xz.mjs → verify-Bp9ZyPgX.mjs} +3 -3
- package/dist/{verify-CZiiby5b.mjs → verify-BwodYH0C.mjs} +3 -3
- package/dist/{wait-DiwWdpia.mjs → wait-DZclBI3D.mjs} +4 -4
- package/dist/{wait-flags-DJK0_BcR.mjs → wait-flags-CmIfm6_U.mjs} +2 -2
- package/dist/{window-D1w7B0vh.mjs → window-BoW2xWwm.mjs} +1 -1
- package/dist/yaml-BthVUNj3.mjs +53 -0
- package/package.json +1 -1
- package/skill-data/data-app/SKILL.md +8 -2
- package/skill-data/data-app/template/package.json +4 -4
- package/skill-data/data-app-data/SKILL.md +2 -0
- package/skill-data/data-app-ui/SKILL.md +3 -0
- package/dist/api-key-Phua-UKC.mjs +0 -13
- package/dist/card-D-TPi4AP.mjs +0 -25
- package/dist/collection-BeEwIJBP.mjs +0 -21
- package/dist/dashboard-REKSjnMI.mjs +0 -35
- package/dist/data-action-BjaLSKed.mjs +0 -24
- package/dist/dependency-DWt1YSeu.mjs +0 -15
- package/dist/document-BiYuO-TS.mjs +0 -20
- package/dist/field-z7nU5at_.mjs +0 -21
- package/dist/git-sync-D-27dQ4p.mjs +0 -29
- package/dist/glossary-Dtoy9bGl.mjs +0 -14
- package/dist/process-BQC3mK2k.mjs +0 -71
- package/dist/snippet-D4Hi7urP.mjs +0 -19
- package/dist/table-CM6cPt2s.mjs +0 -26
- package/dist/timeline-0JJWeOi5.mjs +0 -21
- package/dist/transform-emVHRuKO.mjs +0 -28
- package/dist/transform-job-B81jbOuN.mjs +0 -22
- package/dist/transform-test-CEmn-5-S.mjs +0 -16
- package/dist/upload-CGcx9rCH.mjs +0 -13
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import { c as InternalError, f as ResponseShapeError, h as ValidationError, i as AbortError, o as ConfigError, p as TimeoutError, r as isPlainObject, s as FailedResultError, u as NetworkError } from "./predicates-0C0TsWcJ.mjs";
|
|
2
|
+
import { I as CapabilityError, Y as isHttpNotFound, f as serializeJson, ft as profileFlag, o as jsonLineWriter, ot as connectionFlags, q as HttpError, t as interruptSignal } from "./cli.mjs";
|
|
3
|
+
import { t as defineMetabaseCommand } from "./runtime-Dd2uBKqN.mjs";
|
|
4
|
+
import { o as isTransformTestCode } from "./transform-test-D6QIyu8C.mjs";
|
|
5
|
+
import { a as runProcess, n as ProcessNotFoundError, o as runProcessBinary, r as ProcessTimeoutError, t as ProcessInterruptedError } from "./process-ByKCTa6A.mjs";
|
|
6
|
+
import { t as parseMetabaseYaml } from "./yaml-BthVUNj3.mjs";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { isDeepStrictEqual } from "node:util";
|
|
12
|
+
import { performance } from "node:perf_hooks";
|
|
13
|
+
//#region src/core/ci-coverage.ts
|
|
14
|
+
function byName(a, b) {
|
|
15
|
+
return a.name.localeCompare(b.name);
|
|
16
|
+
}
|
|
17
|
+
function assessCoverage(inputs) {
|
|
18
|
+
const logicAtBase = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const { base } of inputs.changes) if (base?.kind === "transform") logicAtBase.set(base.entityId, base.logic);
|
|
20
|
+
const changed = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const { head } of inputs.changes) {
|
|
22
|
+
if (head?.kind !== "transform" || inputs.testedAtHead.has(head.entityId)) continue;
|
|
23
|
+
const before = logicAtBase.get(head.entityId);
|
|
24
|
+
if (before === void 0) changed.set(head.entityId, {
|
|
25
|
+
entityId: head.entityId,
|
|
26
|
+
name: head.name,
|
|
27
|
+
change: "new"
|
|
28
|
+
});
|
|
29
|
+
else if (!isDeepStrictEqual(before, head.logic)) changed.set(head.entityId, {
|
|
30
|
+
entityId: head.entityId,
|
|
31
|
+
name: head.name,
|
|
32
|
+
change: "edited"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const untouchedUntested = [];
|
|
36
|
+
for (const [entityId, name] of inputs.transformsAtHead) if (!inputs.testedAtHead.has(entityId) && !changed.has(entityId)) untouchedUntested.push({
|
|
37
|
+
entityId,
|
|
38
|
+
name
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
untestedChanges: [...changed.values()].toSorted(byName),
|
|
42
|
+
untouchedUntested: untouchedUntested.toSorted(byName)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const UNTOUCHED_CHECK_ID = "untouched";
|
|
46
|
+
function untouchedWarnings(untouched) {
|
|
47
|
+
if (untouched.length === 0) return [];
|
|
48
|
+
return [{
|
|
49
|
+
code: "no-tests-untouched",
|
|
50
|
+
message: `${untouched.length === 1 ? "1 transform whose logic this change did not touch has" : `${untouched.length} transforms whose logic this change did not touch have`} no tests: ${untouched.map((transform) => transform.name).join(", ")}`
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
function coverageStage(assessment) {
|
|
54
|
+
return {
|
|
55
|
+
name: "lint",
|
|
56
|
+
run: async (report) => {
|
|
57
|
+
for (const transform of assessment.untestedChanges) report({
|
|
58
|
+
check: {
|
|
59
|
+
kind: "coverage",
|
|
60
|
+
id: transform.entityId,
|
|
61
|
+
label: transform.name
|
|
62
|
+
},
|
|
63
|
+
verdict: "failed",
|
|
64
|
+
warnings: [],
|
|
65
|
+
ms: 0,
|
|
66
|
+
detail: {
|
|
67
|
+
change: transform.change,
|
|
68
|
+
message: `${transform.name} is ${transform.change} in this change and has no transform tests`
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const untouched = assessment.untouchedUntested;
|
|
72
|
+
report({
|
|
73
|
+
check: {
|
|
74
|
+
kind: "coverage",
|
|
75
|
+
id: UNTOUCHED_CHECK_ID,
|
|
76
|
+
label: "Transforms whose logic this change did not touch"
|
|
77
|
+
},
|
|
78
|
+
verdict: "passed",
|
|
79
|
+
warnings: untouchedWarnings(untouched),
|
|
80
|
+
ms: 0,
|
|
81
|
+
detail: { untested: untouched.map((transform) => ({
|
|
82
|
+
entity_id: transform.entityId,
|
|
83
|
+
name: transform.name
|
|
84
|
+
})) }
|
|
85
|
+
});
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const StageName = z.enum(["lint", "unit"]);
|
|
91
|
+
const Verdict = z.enum([
|
|
92
|
+
"passed",
|
|
93
|
+
"failed",
|
|
94
|
+
"error",
|
|
95
|
+
"refused"
|
|
96
|
+
]);
|
|
97
|
+
const StageStatus = z.enum([
|
|
98
|
+
"passed",
|
|
99
|
+
"failed",
|
|
100
|
+
"errored",
|
|
101
|
+
"skipped"
|
|
102
|
+
]);
|
|
103
|
+
const JobState = StageStatus.exclude(["skipped"]);
|
|
104
|
+
const CiWarning = z.object({
|
|
105
|
+
code: z.string(),
|
|
106
|
+
message: z.string()
|
|
107
|
+
});
|
|
108
|
+
const StageStop = z.object({
|
|
109
|
+
code: z.string(),
|
|
110
|
+
message: z.string()
|
|
111
|
+
});
|
|
112
|
+
const CheckedTransform = z.object({
|
|
113
|
+
entity_id: z.string(),
|
|
114
|
+
name: z.string()
|
|
115
|
+
});
|
|
116
|
+
const Snapshot = z.object({
|
|
117
|
+
base: z.string(),
|
|
118
|
+
head: z.string()
|
|
119
|
+
});
|
|
120
|
+
const Check = z.object({
|
|
121
|
+
kind: z.enum([
|
|
122
|
+
"coverage",
|
|
123
|
+
"yaml",
|
|
124
|
+
"cycles",
|
|
125
|
+
"broken-refs",
|
|
126
|
+
"transform-test"
|
|
127
|
+
]),
|
|
128
|
+
id: z.string(),
|
|
129
|
+
label: z.string()
|
|
130
|
+
});
|
|
131
|
+
const EventEnvelope = z.object({
|
|
132
|
+
seq: z.number().int().nonnegative(),
|
|
133
|
+
job: z.string(),
|
|
134
|
+
at: z.iso.datetime()
|
|
135
|
+
});
|
|
136
|
+
const JobStart = EventEnvelope.extend({
|
|
137
|
+
type: z.literal("job.start"),
|
|
138
|
+
v: z.literal(1),
|
|
139
|
+
snapshot: Snapshot,
|
|
140
|
+
checked: z.array(CheckedTransform),
|
|
141
|
+
warnings: z.array(CiWarning)
|
|
142
|
+
});
|
|
143
|
+
const StageStart = EventEnvelope.extend({
|
|
144
|
+
type: z.literal("stage.start"),
|
|
145
|
+
stage: StageName
|
|
146
|
+
});
|
|
147
|
+
const CheckResult = EventEnvelope.extend({
|
|
148
|
+
type: z.literal("check.result"),
|
|
149
|
+
stage: StageName,
|
|
150
|
+
check: Check,
|
|
151
|
+
verdict: Verdict,
|
|
152
|
+
warnings: z.array(CiWarning),
|
|
153
|
+
ms: z.number().nonnegative(),
|
|
154
|
+
detail: z.unknown()
|
|
155
|
+
});
|
|
156
|
+
const StageEnd = EventEnvelope.extend({
|
|
157
|
+
type: z.literal("stage.end"),
|
|
158
|
+
stage: StageName,
|
|
159
|
+
status: StageStatus,
|
|
160
|
+
stopped: StageStop.nullable()
|
|
161
|
+
});
|
|
162
|
+
const JobEnd = EventEnvelope.extend({
|
|
163
|
+
type: z.literal("job.end"),
|
|
164
|
+
state: JobState,
|
|
165
|
+
counts: z.object({
|
|
166
|
+
checks: z.number().int(),
|
|
167
|
+
warnings: z.number().int()
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
const CiEvent = z.discriminatedUnion("type", [
|
|
171
|
+
JobStart,
|
|
172
|
+
StageStart,
|
|
173
|
+
CheckResult,
|
|
174
|
+
StageEnd,
|
|
175
|
+
JobEnd
|
|
176
|
+
]);
|
|
177
|
+
function createEmitter(options) {
|
|
178
|
+
let seq = 0;
|
|
179
|
+
return (body) => {
|
|
180
|
+
const parsed = CiEvent.safeParse({
|
|
181
|
+
...body,
|
|
182
|
+
seq,
|
|
183
|
+
job: options.job,
|
|
184
|
+
at: options.now().toISOString()
|
|
185
|
+
});
|
|
186
|
+
if (!parsed.success) throw new InternalError(`ci event "${body.type}" does not match its schema: ${parsed.error.message}`);
|
|
187
|
+
seq += 1;
|
|
188
|
+
options.write(parsed.data);
|
|
189
|
+
return parsed.data;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function stageStatus(verdicts, stopped) {
|
|
193
|
+
if (verdicts.includes("failed")) return "failed";
|
|
194
|
+
if (stopped !== null || verdicts.includes("refused") || verdicts.includes("error")) return "errored";
|
|
195
|
+
return "passed";
|
|
196
|
+
}
|
|
197
|
+
async function runStages(stages, emit) {
|
|
198
|
+
let state = "passed";
|
|
199
|
+
let checks = 0;
|
|
200
|
+
let warnings = 0;
|
|
201
|
+
for (const stage of stages) {
|
|
202
|
+
if (state !== "passed") {
|
|
203
|
+
emit({
|
|
204
|
+
type: "stage.end",
|
|
205
|
+
stage: stage.name,
|
|
206
|
+
status: "skipped",
|
|
207
|
+
stopped: null
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
emit({
|
|
212
|
+
type: "stage.start",
|
|
213
|
+
stage: stage.name
|
|
214
|
+
});
|
|
215
|
+
const verdicts = [];
|
|
216
|
+
const stopped = await stage.run((check) => {
|
|
217
|
+
verdicts.push(check.verdict);
|
|
218
|
+
checks += 1;
|
|
219
|
+
warnings += check.warnings.length;
|
|
220
|
+
emit({
|
|
221
|
+
type: "check.result",
|
|
222
|
+
stage: stage.name,
|
|
223
|
+
...check
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
state = stageStatus(verdicts, stopped);
|
|
227
|
+
emit({
|
|
228
|
+
type: "stage.end",
|
|
229
|
+
stage: stage.name,
|
|
230
|
+
status: state,
|
|
231
|
+
stopped
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
state,
|
|
236
|
+
checks,
|
|
237
|
+
warnings
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/core/ci-affected.ts
|
|
242
|
+
const SINGLE_PATH_STATUSES = /* @__PURE__ */ new Map([
|
|
243
|
+
["A", "added"],
|
|
244
|
+
["M", "modified"],
|
|
245
|
+
["D", "deleted"],
|
|
246
|
+
["T", "modified"]
|
|
247
|
+
]);
|
|
248
|
+
const STATUS_CODE = /^(?:[AMDT](?:\d{3})?|[RC]\d{3})$/;
|
|
249
|
+
const RENAME_STATUS = "R";
|
|
250
|
+
const COPY_STATUS = "C";
|
|
251
|
+
function nameStatusError(message) {
|
|
252
|
+
return new InternalError(`git diff --name-status -z: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
function requireToken(token, what) {
|
|
255
|
+
if (token === void 0 || token === "") throw nameStatusError(`missing ${what}`);
|
|
256
|
+
return token;
|
|
257
|
+
}
|
|
258
|
+
function parseNameStatus(output) {
|
|
259
|
+
if (output === "") return [];
|
|
260
|
+
if (!output.endsWith("\0")) throw nameStatusError("truncated: the output does not end in a NUL");
|
|
261
|
+
const tokens = output.slice(0, -1).split("\0");
|
|
262
|
+
const entries = [];
|
|
263
|
+
let index = 0;
|
|
264
|
+
while (index < tokens.length) {
|
|
265
|
+
const code = requireToken(tokens[index], "a status");
|
|
266
|
+
if (!STATUS_CODE.test(code)) throw nameStatusError(`unexpected status "${code}"`);
|
|
267
|
+
const missingPath = `a path for status "${code}"`;
|
|
268
|
+
const first = tokens[index + 1];
|
|
269
|
+
const second = tokens[index + 2];
|
|
270
|
+
const letter = code.charAt(0);
|
|
271
|
+
const single = SINGLE_PATH_STATUSES.get(letter);
|
|
272
|
+
if (single !== void 0) {
|
|
273
|
+
entries.push({
|
|
274
|
+
status: single,
|
|
275
|
+
path: requireToken(first, missingPath)
|
|
276
|
+
});
|
|
277
|
+
index += 2;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (letter === RENAME_STATUS) {
|
|
281
|
+
entries.push({
|
|
282
|
+
status: "renamed",
|
|
283
|
+
path: requireToken(second, missingPath),
|
|
284
|
+
oldPath: requireToken(first, missingPath)
|
|
285
|
+
});
|
|
286
|
+
index += 3;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (letter === COPY_STATUS) {
|
|
290
|
+
requireToken(first, missingPath);
|
|
291
|
+
entries.push({
|
|
292
|
+
status: "added",
|
|
293
|
+
path: requireToken(second, missingPath)
|
|
294
|
+
});
|
|
295
|
+
index += 3;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
throw nameStatusError(`unexpected status "${code}"`);
|
|
299
|
+
}
|
|
300
|
+
return entries;
|
|
301
|
+
}
|
|
302
|
+
const PORCELAIN_STATUS_WIDTH = 2;
|
|
303
|
+
const PORCELAIN_SOURCE_STATUSES = /* @__PURE__ */ new Set(["R", "C"]);
|
|
304
|
+
function parseStatusPorcelain(output) {
|
|
305
|
+
if (output === "") return [];
|
|
306
|
+
if (!output.endsWith("\0")) throw new InternalError("git status --porcelain -z: truncated: the output does not end in a NUL");
|
|
307
|
+
const paths = [];
|
|
308
|
+
let sourceFollows = false;
|
|
309
|
+
for (const record of output.slice(0, -1).split("\0")) {
|
|
310
|
+
if (sourceFollows) {
|
|
311
|
+
sourceFollows = false;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const status = record.slice(0, PORCELAIN_STATUS_WIDTH);
|
|
315
|
+
const path = record.slice(3);
|
|
316
|
+
if (status.length !== PORCELAIN_STATUS_WIDTH || record.charAt(PORCELAIN_STATUS_WIDTH) !== " " || path === "") throw new InternalError(`git status --porcelain -z: malformed entry "${record}"`);
|
|
317
|
+
paths.push(path);
|
|
318
|
+
sourceFollows = [...status].some((letter) => PORCELAIN_SOURCE_STATUSES.has(letter));
|
|
319
|
+
}
|
|
320
|
+
return paths;
|
|
321
|
+
}
|
|
322
|
+
const SERDES_META_KEY = "serdes/meta";
|
|
323
|
+
const TransformDocument = z.object({
|
|
324
|
+
entity_id: z.string().min(1),
|
|
325
|
+
name: z.string().min(1),
|
|
326
|
+
source: z.unknown(),
|
|
327
|
+
target: z.unknown()
|
|
328
|
+
}).loose();
|
|
329
|
+
const TransformTestDocument = z.object({ transform_id: z.string().min(1) }).loose();
|
|
330
|
+
const SerdesMeta = z.array(z.object({ model: z.string() }).loose()).min(1);
|
|
331
|
+
function parseOrRefuse(path, schema, value, rule) {
|
|
332
|
+
const parsed = schema.safeParse(value);
|
|
333
|
+
if (!parsed.success) throw new ValidationError(`${path}: ${rule}`, {
|
|
334
|
+
source: path,
|
|
335
|
+
zodIssues: parsed.error.issues
|
|
336
|
+
});
|
|
337
|
+
return parsed.data;
|
|
338
|
+
}
|
|
339
|
+
function classifyContentFile(path, document) {
|
|
340
|
+
if (!isPlainObject(document) || !(SERDES_META_KEY in document)) return { kind: "other" };
|
|
341
|
+
const last = parseOrRefuse(path, SerdesMeta, document[SERDES_META_KEY], `${SERDES_META_KEY} must be a non-empty list of entries with a model`).at(-1);
|
|
342
|
+
if (last === void 0) throw new InternalError(`${path}: ${SERDES_META_KEY} passed a non-empty check but has no entries`);
|
|
343
|
+
if (last.model === "Transform") {
|
|
344
|
+
const transform = parseOrRefuse(path, TransformDocument, document, "a Transform must have an entity_id, a name, a source and a target");
|
|
345
|
+
return {
|
|
346
|
+
kind: "transform",
|
|
347
|
+
entityId: transform.entity_id,
|
|
348
|
+
name: transform.name,
|
|
349
|
+
logic: {
|
|
350
|
+
source: transform.source,
|
|
351
|
+
target: transform.target
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (last.model === "TransformTest") return {
|
|
356
|
+
kind: "transform-test",
|
|
357
|
+
transformId: parseOrRefuse(path, TransformTestDocument, document, "a TransformTest must name its transform_id").transform_id
|
|
358
|
+
};
|
|
359
|
+
return { kind: "other" };
|
|
360
|
+
}
|
|
361
|
+
function transformEntityId(file) {
|
|
362
|
+
return file?.kind === "transform" ? file.entityId : null;
|
|
363
|
+
}
|
|
364
|
+
function affectedTransforms(changes) {
|
|
365
|
+
const touched = /* @__PURE__ */ new Set();
|
|
366
|
+
const atHead = /* @__PURE__ */ new Set();
|
|
367
|
+
const atBase = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const { path, base, head } of changes) {
|
|
369
|
+
if (base !== null && base.kind !== "other" && head?.kind === "other") throw new ValidationError(`${path}: a Transform or TransformTest at base no longer reads as one at head`, {
|
|
370
|
+
source: path,
|
|
371
|
+
zodIssues: []
|
|
372
|
+
});
|
|
373
|
+
const headId = transformEntityId(head);
|
|
374
|
+
const baseId = transformEntityId(base);
|
|
375
|
+
if (headId !== null) {
|
|
376
|
+
touched.add(headId);
|
|
377
|
+
atHead.add(headId);
|
|
378
|
+
}
|
|
379
|
+
if (baseId !== null) atBase.add(baseId);
|
|
380
|
+
for (const side of [base, head]) if (side?.kind === "transform-test") touched.add(side.transformId);
|
|
381
|
+
}
|
|
382
|
+
return [...touched].filter((entityId) => atHead.has(entityId) || !atBase.has(entityId));
|
|
383
|
+
}
|
|
384
|
+
const TRANSFORM_MODEL_PATTERN = `model["']?:[[:space:]]*["']?Transform`;
|
|
385
|
+
function headTransforms(files) {
|
|
386
|
+
const atHead = {
|
|
387
|
+
names: /* @__PURE__ */ new Map(),
|
|
388
|
+
tested: /* @__PURE__ */ new Set()
|
|
389
|
+
};
|
|
390
|
+
for (const file of files) if (file?.kind === "transform") atHead.names.set(file.entityId, file.name);
|
|
391
|
+
else if (file?.kind === "transform-test") atHead.tested.add(file.transformId);
|
|
392
|
+
return atHead;
|
|
393
|
+
}
|
|
394
|
+
const CONTENT_TOP_LEVEL_FOLDERS = [
|
|
395
|
+
"actions",
|
|
396
|
+
"channels",
|
|
397
|
+
"collections",
|
|
398
|
+
"custom_viz_plugins",
|
|
399
|
+
"data_apps",
|
|
400
|
+
"databases",
|
|
401
|
+
"embedding_themes",
|
|
402
|
+
"glossary",
|
|
403
|
+
"metabots",
|
|
404
|
+
"python_libraries",
|
|
405
|
+
"python-libraries",
|
|
406
|
+
"osi_ai_context",
|
|
407
|
+
"snippets",
|
|
408
|
+
"transforms"
|
|
409
|
+
];
|
|
410
|
+
const SHARED_TOP_LEVEL_FILE_NAMES = /* @__PURE__ */ new Map([["data_apps", "data_app.yaml"]]);
|
|
411
|
+
const CONTENT_FOLDERS = new Set(CONTENT_TOP_LEVEL_FOLDERS);
|
|
412
|
+
function isContentPath(path) {
|
|
413
|
+
const slash = path.indexOf("/");
|
|
414
|
+
if (slash === -1) return false;
|
|
415
|
+
const top = path.slice(0, slash);
|
|
416
|
+
const fileName = path.slice(path.lastIndexOf("/") + 1);
|
|
417
|
+
if (!fileName.endsWith(".yaml") || fileName.startsWith(".") || !CONTENT_FOLDERS.has(top)) return false;
|
|
418
|
+
const entityFileName = SHARED_TOP_LEVEL_FILE_NAMES.get(top);
|
|
419
|
+
return entityFileName === void 0 || entityFileName === fileName;
|
|
420
|
+
}
|
|
421
|
+
function noContentChangesWarning(entries) {
|
|
422
|
+
if (entries.some((entry) => isContentPath(entry.path) || entry.status === "renamed" && isContentPath(entry.oldPath))) return null;
|
|
423
|
+
return {
|
|
424
|
+
code: "no-content-changes",
|
|
425
|
+
message: "the change touches no Metabase content files, so CI checked nothing"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function uncommittedContentWarning(paths) {
|
|
429
|
+
const content = paths.filter(isContentPath);
|
|
430
|
+
if (content.length === 0) return null;
|
|
431
|
+
return {
|
|
432
|
+
code: "uncommitted-content",
|
|
433
|
+
message: `${content.length === 1 ? "1 content file has uncommitted changes" : `${content.length} content files have uncommitted changes`}, which CI does not see; it checks the committed version: ${content.join(", ")}`
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/runtime/git.ts
|
|
438
|
+
const GIT = "git";
|
|
439
|
+
const NO_MATCH_EXIT_CODE = 1;
|
|
440
|
+
const NO_MERGE_BASE_EXIT_CODE = 1;
|
|
441
|
+
const UNKNOWN_REF_EXIT_CODE = 1;
|
|
442
|
+
const GIT_TIMEOUT_MS = 12e4;
|
|
443
|
+
async function withGit(repo, args, spawn) {
|
|
444
|
+
try {
|
|
445
|
+
return await spawn();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (error instanceof ProcessNotFoundError) throw new ConfigError("git is not installed or not on PATH");
|
|
448
|
+
if (error instanceof ProcessInterruptedError) throw new AbortError(`interrupted while git ${args.join(" ")} ran in ${repo}`);
|
|
449
|
+
if (error instanceof ProcessTimeoutError) throw new ConfigError(`git ${args.join(" ")} did not finish within ${GIT_TIMEOUT_MS / 1e3} s in ${repo}`);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function runGit(repo, args, signal) {
|
|
454
|
+
return withGit(repo, args, () => runProcess(GIT, [
|
|
455
|
+
"-C",
|
|
456
|
+
repo,
|
|
457
|
+
...args
|
|
458
|
+
], {
|
|
459
|
+
signal,
|
|
460
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
async function git(repo, args, signal) {
|
|
464
|
+
const result = await runGit(repo, args, signal);
|
|
465
|
+
if (result.exitCode !== 0) throw new ConfigError(`git ${args.join(" ")} failed in ${repo}: ${result.stderr.trim()}`);
|
|
466
|
+
return result.stdout;
|
|
467
|
+
}
|
|
468
|
+
async function repositoryRoot(repo, signal) {
|
|
469
|
+
const result = await runGit(repo, ["rev-parse", "--show-toplevel"], signal);
|
|
470
|
+
if (result.exitCode !== 0) throw new ConfigError(`--repo ${repo} is not inside a git work tree: ${result.stderr.trim()}`);
|
|
471
|
+
return result.stdout.trim();
|
|
472
|
+
}
|
|
473
|
+
async function resolveCommit(repo, ref, flag, signal) {
|
|
474
|
+
const result = await runGit(repo, [
|
|
475
|
+
"rev-parse",
|
|
476
|
+
"--verify",
|
|
477
|
+
"--quiet",
|
|
478
|
+
"--end-of-options",
|
|
479
|
+
`${ref}^{commit}`
|
|
480
|
+
], signal);
|
|
481
|
+
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) throw new ConfigError(`${flag} ${ref} is not a commit in ${repo}`);
|
|
482
|
+
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse failed in ${repo}: ${result.stderr.trim()}`);
|
|
483
|
+
return result.stdout.trim();
|
|
484
|
+
}
|
|
485
|
+
async function checkedOutCommit(repo, signal) {
|
|
486
|
+
const result = await runGit(repo, [
|
|
487
|
+
"rev-parse",
|
|
488
|
+
"--verify",
|
|
489
|
+
"--quiet",
|
|
490
|
+
"HEAD"
|
|
491
|
+
], signal);
|
|
492
|
+
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) return null;
|
|
493
|
+
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse HEAD failed in ${repo}: ${result.stderr.trim()}`);
|
|
494
|
+
return result.stdout.trim();
|
|
495
|
+
}
|
|
496
|
+
async function mergeBase(repo, base, head, signal) {
|
|
497
|
+
const result = await runGit(repo, [
|
|
498
|
+
"merge-base",
|
|
499
|
+
base,
|
|
500
|
+
head
|
|
501
|
+
], signal);
|
|
502
|
+
if (result.exitCode === NO_MERGE_BASE_EXIT_CODE) throw new ConfigError(`--base and --head share no history in ${repo}: the clone may be shallow (fetch the full history, e.g. actions/checkout with fetch-depth: 0), or the histories are unrelated`);
|
|
503
|
+
if (result.exitCode !== 0) throw new ConfigError(`git merge-base failed in ${repo}: ${result.stderr.trim()}`);
|
|
504
|
+
return result.stdout.trim();
|
|
505
|
+
}
|
|
506
|
+
function diffNameStatus(repo, from, to, signal) {
|
|
507
|
+
return git(repo, [
|
|
508
|
+
"diff",
|
|
509
|
+
"--name-status",
|
|
510
|
+
"-z",
|
|
511
|
+
"-M",
|
|
512
|
+
from,
|
|
513
|
+
to
|
|
514
|
+
], signal);
|
|
515
|
+
}
|
|
516
|
+
function statusPorcelain(repo, signal) {
|
|
517
|
+
return git(repo, [
|
|
518
|
+
"status",
|
|
519
|
+
"--porcelain",
|
|
520
|
+
"-z",
|
|
521
|
+
"--untracked-files=all"
|
|
522
|
+
], signal);
|
|
523
|
+
}
|
|
524
|
+
async function fileModesAt(repo, commit, signal) {
|
|
525
|
+
const output = await git(repo, [
|
|
526
|
+
"ls-tree",
|
|
527
|
+
"-r",
|
|
528
|
+
"-z",
|
|
529
|
+
"--full-tree",
|
|
530
|
+
commit
|
|
531
|
+
], signal);
|
|
532
|
+
const modes = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const record of output.split("\0")) {
|
|
534
|
+
if (record === "") continue;
|
|
535
|
+
const space = record.indexOf(" ");
|
|
536
|
+
const tab = record.indexOf(" ");
|
|
537
|
+
if (space === -1 || tab < space) throw new InternalError(`git ls-tree -z: malformed entry "${record}"`);
|
|
538
|
+
modes.set(record.slice(tab + 1), record.slice(0, space));
|
|
539
|
+
}
|
|
540
|
+
return modes;
|
|
541
|
+
}
|
|
542
|
+
async function filesMatching(repo, commit, pattern, pathspecs, signal) {
|
|
543
|
+
const result = await runGit(repo, [
|
|
544
|
+
"grep",
|
|
545
|
+
"--no-color",
|
|
546
|
+
"-z",
|
|
547
|
+
"-l",
|
|
548
|
+
"-E",
|
|
549
|
+
"-e",
|
|
550
|
+
pattern,
|
|
551
|
+
commit,
|
|
552
|
+
"--",
|
|
553
|
+
...pathspecs
|
|
554
|
+
], signal);
|
|
555
|
+
if (result.exitCode === NO_MATCH_EXIT_CODE) return [];
|
|
556
|
+
if (result.exitCode !== 0) throw new ConfigError(`git grep failed in ${repo}: ${result.stderr.trim()}`);
|
|
557
|
+
const prefix = `${commit}:`;
|
|
558
|
+
return result.stdout.split("\0").filter((entry) => entry !== "").map((entry) => {
|
|
559
|
+
if (!entry.startsWith(prefix)) throw new InternalError(`git grep printed "${entry}", which does not start with ${prefix}`);
|
|
560
|
+
return entry.slice(prefix.length);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const NEWLINE = 10;
|
|
564
|
+
const utf8 = new TextDecoder("utf-8");
|
|
565
|
+
async function readFilesAt(repo, commit, paths, signal) {
|
|
566
|
+
const files = /* @__PURE__ */ new Map();
|
|
567
|
+
if (paths.length === 0) return files;
|
|
568
|
+
const multiline = paths.find((path) => path.includes("\n"));
|
|
569
|
+
if (multiline !== void 0) throw new ConfigError(`cannot read ${JSON.stringify(multiline)}: a path with a newline cannot be batched`);
|
|
570
|
+
const args = ["cat-file", "--batch"];
|
|
571
|
+
const result = await withGit(repo, args, () => runProcessBinary(GIT, [
|
|
572
|
+
"-C",
|
|
573
|
+
repo,
|
|
574
|
+
...args
|
|
575
|
+
], {
|
|
576
|
+
stdin: paths.map((path) => `${commit}:${path}\n`).join(""),
|
|
577
|
+
signal,
|
|
578
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
579
|
+
}));
|
|
580
|
+
if (result.exitCode !== 0) throw new ConfigError(`git cat-file --batch failed in ${repo}: ${result.stderr.trim()}`);
|
|
581
|
+
const output = result.stdout;
|
|
582
|
+
let offset = 0;
|
|
583
|
+
for (const path of paths) {
|
|
584
|
+
const headerEnd = output.indexOf(NEWLINE, offset);
|
|
585
|
+
if (headerEnd === -1) throw new InternalError(`git cat-file --batch stopped before ${path}`);
|
|
586
|
+
const header = utf8.decode(output.subarray(offset, headerEnd)).split(" ");
|
|
587
|
+
const size = Number(header[2]);
|
|
588
|
+
if (header[1] !== "blob" || !Number.isInteger(size)) throw new InternalError(`${path} is not a file at ${commit}, though git listed it there`);
|
|
589
|
+
const start = headerEnd + 1;
|
|
590
|
+
if (output[start + size] !== NEWLINE) throw new InternalError(`git cat-file --batch framed ${path} with ${size} bytes, but no newline follows them`);
|
|
591
|
+
files.set(path, utf8.decode(output.subarray(start, start + size)));
|
|
592
|
+
offset = start + size + 1;
|
|
593
|
+
}
|
|
594
|
+
if (offset !== output.length) throw new InternalError("git cat-file --batch printed more than the paths it was asked for");
|
|
595
|
+
return files;
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/commands/ci/job-plan.ts
|
|
599
|
+
const CONTENT_PATHSPECS = CONTENT_TOP_LEVEL_FOLDERS.map((folder) => `${folder}/*.yaml`);
|
|
600
|
+
const FILE_MODES = /* @__PURE__ */ new Set(["100644", "100755"]);
|
|
601
|
+
async function classifyAt(repo, commit, paths, signal) {
|
|
602
|
+
const content = paths.filter(isContentPath);
|
|
603
|
+
if (content.length === 0) return /* @__PURE__ */ new Map();
|
|
604
|
+
const modes = await fileModesAt(repo, commit, signal);
|
|
605
|
+
for (const path of content) {
|
|
606
|
+
const mode = modes.get(path);
|
|
607
|
+
if (mode === void 0) throw new InternalError(`git listed ${path} at ${commit}, but its tree does not hold it`);
|
|
608
|
+
if (!FILE_MODES.has(mode)) throw new ValidationError(`${commit}:${path} is not a regular file (git mode ${mode}): Metabase would import what it points to, which this check cannot read; commit the file itself`, {
|
|
609
|
+
source: `${commit}:${path}`,
|
|
610
|
+
zodIssues: []
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const classified = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const [path, text] of await readFilesAt(repo, commit, content, signal)) classified.set(path, classifyContentFile(path, parseMetabaseYaml(text, `${commit}:${path}`)));
|
|
615
|
+
return classified;
|
|
616
|
+
}
|
|
617
|
+
function basePath(entry) {
|
|
618
|
+
if (entry.status === "added") return null;
|
|
619
|
+
return entry.status === "renamed" ? entry.oldPath : entry.path;
|
|
620
|
+
}
|
|
621
|
+
function headPath(entry) {
|
|
622
|
+
return entry.status === "deleted" ? null : entry.path;
|
|
623
|
+
}
|
|
624
|
+
function present(paths) {
|
|
625
|
+
return paths.filter((path) => path !== null);
|
|
626
|
+
}
|
|
627
|
+
function sideOf(files, path) {
|
|
628
|
+
if (path === null || !isContentPath(path)) return null;
|
|
629
|
+
const file = files.get(path);
|
|
630
|
+
if (file === void 0) throw new InternalError(`${path} was not read, though the diff names it`);
|
|
631
|
+
return file;
|
|
632
|
+
}
|
|
633
|
+
async function classifyChanges(repo, forkPoint, head, entries, signal) {
|
|
634
|
+
const [baseFiles, headFiles] = await Promise.all([classifyAt(repo, forkPoint, present(entries.map(basePath)), signal), classifyAt(repo, head, present(entries.map(headPath)), signal)]);
|
|
635
|
+
return entries.map((entry) => ({
|
|
636
|
+
path: entry.path,
|
|
637
|
+
base: sideOf(baseFiles, basePath(entry)),
|
|
638
|
+
head: sideOf(headFiles, headPath(entry))
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
async function scanHead(repo, head, changes, changedPaths, signal) {
|
|
642
|
+
const unchangedFiles = (await classifyAt(repo, head, (await filesMatching(repo, head, TRANSFORM_MODEL_PATTERN, CONTENT_PATHSPECS, signal)).filter((path) => !changedPaths.has(path)), signal)).values();
|
|
643
|
+
return headTransforms([...changes.map((change) => change.head), ...unchangedFiles]);
|
|
644
|
+
}
|
|
645
|
+
async function planJob(repoPath, baseRef, headRef, signal) {
|
|
646
|
+
const repo = await repositoryRoot(repoPath, signal);
|
|
647
|
+
const [base, head, checkout] = await Promise.all([
|
|
648
|
+
resolveCommit(repo, baseRef, "--base", signal),
|
|
649
|
+
resolveCommit(repo, headRef, "--head", signal),
|
|
650
|
+
checkedOutCommit(repo, signal)
|
|
651
|
+
]);
|
|
652
|
+
const forkPoint = await mergeBase(repo, base, head, signal);
|
|
653
|
+
const entries = parseNameStatus(await diffNameStatus(repo, forkPoint, head, signal));
|
|
654
|
+
const changes = await classifyChanges(repo, forkPoint, head, entries, signal);
|
|
655
|
+
const atHead = await scanHead(repo, head, changes, new Set(entries.map((entry) => entry.path)), signal);
|
|
656
|
+
const checked = [];
|
|
657
|
+
const warnings = [];
|
|
658
|
+
const noContent = noContentChangesWarning(entries);
|
|
659
|
+
if (noContent !== null) warnings.push(noContent);
|
|
660
|
+
for (const entityId of affectedTransforms(changes)) {
|
|
661
|
+
const name = atHead.names.get(entityId);
|
|
662
|
+
if (name === void 0) {
|
|
663
|
+
warnings.push({
|
|
664
|
+
code: "missing-transform",
|
|
665
|
+
message: `a changed transform test covers transform ${entityId}, which is not in the repo at ${head}`
|
|
666
|
+
});
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
checked.push({
|
|
670
|
+
entity_id: entityId,
|
|
671
|
+
name
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
const coverage = assessCoverage({
|
|
675
|
+
changes,
|
|
676
|
+
transformsAtHead: atHead.names,
|
|
677
|
+
testedAtHead: atHead.tested
|
|
678
|
+
});
|
|
679
|
+
if (head === checkout) {
|
|
680
|
+
const uncommitted = uncommittedContentWarning(parseStatusPorcelain(await statusPorcelain(repo, signal)));
|
|
681
|
+
if (uncommitted !== null) warnings.push(uncommitted);
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
snapshot: {
|
|
685
|
+
base,
|
|
686
|
+
head
|
|
687
|
+
},
|
|
688
|
+
checked,
|
|
689
|
+
warnings,
|
|
690
|
+
coverage
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/commands/ci/job.ts
|
|
695
|
+
async function writeResults(path, events) {
|
|
696
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
697
|
+
await writeFile(temporary, serializeJson(events, false) + "\n", {
|
|
698
|
+
encoding: "utf8",
|
|
699
|
+
flag: "wx"
|
|
700
|
+
});
|
|
701
|
+
try {
|
|
702
|
+
await rename(temporary, path);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
await rm(temporary, { force: true });
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function runJob(options) {
|
|
709
|
+
if (options.resultsPath !== null) await rm(options.resultsPath, { force: true });
|
|
710
|
+
const events = [];
|
|
711
|
+
const emit = createEmitter({
|
|
712
|
+
job: options.job,
|
|
713
|
+
now: options.now,
|
|
714
|
+
write: (event) => {
|
|
715
|
+
events.push(event);
|
|
716
|
+
if (event.type !== "job.end") options.write(event);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
const plan = await planJob(options.repo, options.base, options.head, options.signal);
|
|
720
|
+
emit({
|
|
721
|
+
type: "job.start",
|
|
722
|
+
v: 1,
|
|
723
|
+
snapshot: plan.snapshot,
|
|
724
|
+
checked: plan.checked,
|
|
725
|
+
warnings: plan.warnings
|
|
726
|
+
});
|
|
727
|
+
const outcome = await runStages(await options.stages(plan), emit);
|
|
728
|
+
const end = emit({
|
|
729
|
+
type: "job.end",
|
|
730
|
+
state: outcome.state,
|
|
731
|
+
counts: {
|
|
732
|
+
checks: outcome.checks,
|
|
733
|
+
warnings: outcome.warnings + plan.warnings.length
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
if (options.resultsPath !== null) await writeResults(options.resultsPath, events);
|
|
737
|
+
options.write(end);
|
|
738
|
+
return outcome.state;
|
|
739
|
+
}
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/core/ci-unit.ts
|
|
742
|
+
function testVerdict(result) {
|
|
743
|
+
const statuses = new Set(result.expectations.map((expectation) => expectation.status));
|
|
744
|
+
if (statuses.has("failed")) return "failed";
|
|
745
|
+
if (statuses.has("error")) return "error";
|
|
746
|
+
return result.status;
|
|
747
|
+
}
|
|
748
|
+
function testRefusal(error) {
|
|
749
|
+
if (!(error instanceof HttpError) || error.errorCode === null || !isTransformTestCode(error.errorCode)) return null;
|
|
750
|
+
return {
|
|
751
|
+
error_code: error.errorCode,
|
|
752
|
+
message: error.serverMessage ?? error.message,
|
|
753
|
+
tables: error.tables
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
const CHANGE_FAILURE_CODES = /* @__PURE__ */ new Set(["transform-test.transform-failed", "transform-test.unparseable-source"]);
|
|
757
|
+
function refusalVerdict(refusal) {
|
|
758
|
+
return CHANGE_FAILURE_CODES.has(refusal.error_code) ? "failed" : "refused";
|
|
759
|
+
}
|
|
760
|
+
const TRANSFORM_NOT_ON_INSTANCE = {
|
|
761
|
+
error_code: "ci.transform-not-on-instance",
|
|
762
|
+
message: "the transform is in the branch but not on the Metabase instance; load the branch into it first"
|
|
763
|
+
};
|
|
764
|
+
const INVALID_ENTITY_ID = {
|
|
765
|
+
error_code: "ci.invalid-entity-id",
|
|
766
|
+
message: "the transform's entity_id in the branch is not a valid entity id"
|
|
767
|
+
};
|
|
768
|
+
const TRANSFORM_UNREADABLE = {
|
|
769
|
+
error_code: "ci.transform-unreadable",
|
|
770
|
+
message: "the CI identity cannot read this transform, so Metabase hides its tests: reading it needs query permission on every table it reads"
|
|
771
|
+
};
|
|
772
|
+
const TEST_RUN_FORBIDDEN = {
|
|
773
|
+
error_code: "ci.test-run-forbidden",
|
|
774
|
+
message: "Metabase did not let the CI identity run this test: running a test needs write access to it, and a read-only remote-synced instance refuses everyone"
|
|
775
|
+
};
|
|
776
|
+
function requestFailure(error) {
|
|
777
|
+
if (error instanceof HttpError) return {
|
|
778
|
+
error_code: "ci.http-error",
|
|
779
|
+
status: error.status,
|
|
780
|
+
message: error.message
|
|
781
|
+
};
|
|
782
|
+
if (error instanceof TimeoutError) return {
|
|
783
|
+
error_code: "ci.timeout",
|
|
784
|
+
status: null,
|
|
785
|
+
message: error.message
|
|
786
|
+
};
|
|
787
|
+
if (error instanceof NetworkError) return {
|
|
788
|
+
error_code: "ci.network-error",
|
|
789
|
+
status: null,
|
|
790
|
+
message: error.message
|
|
791
|
+
};
|
|
792
|
+
if (error instanceof ResponseShapeError) return {
|
|
793
|
+
error_code: "ci.unreadable-answer",
|
|
794
|
+
status: null,
|
|
795
|
+
message: error.message
|
|
796
|
+
};
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
function sessionStop(error) {
|
|
800
|
+
if (error instanceof CapabilityError) return {
|
|
801
|
+
code: "ci.unsupported-server",
|
|
802
|
+
message: error.message
|
|
803
|
+
};
|
|
804
|
+
if (error instanceof HttpError && error.kind === "auth") return {
|
|
805
|
+
code: "ci.unauthorized",
|
|
806
|
+
message: error.message
|
|
807
|
+
};
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
function lookupStop(error) {
|
|
811
|
+
const stop = sessionStop(error);
|
|
812
|
+
if (stop !== null) return stop;
|
|
813
|
+
const failure = requestFailure(error);
|
|
814
|
+
return failure === null ? null : {
|
|
815
|
+
code: failure.error_code,
|
|
816
|
+
message: failure.message
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const REFUSAL_FOR_FORBIDDEN = {
|
|
820
|
+
read: TRANSFORM_UNREADABLE,
|
|
821
|
+
list: null,
|
|
822
|
+
run: TEST_RUN_FORBIDDEN
|
|
823
|
+
};
|
|
824
|
+
function transformFailure(error, request) {
|
|
825
|
+
const stop = sessionStop(error);
|
|
826
|
+
if (stop !== null) return {
|
|
827
|
+
kind: "stop",
|
|
828
|
+
stop
|
|
829
|
+
};
|
|
830
|
+
const refusal = request === "run" ? testRefusal(error) : null;
|
|
831
|
+
if (refusal !== null) return {
|
|
832
|
+
kind: "check",
|
|
833
|
+
verdict: refusalVerdict(refusal),
|
|
834
|
+
detail: refusal
|
|
835
|
+
};
|
|
836
|
+
const forbiddenRefusal = error instanceof HttpError && error.kind === "forbidden" ? REFUSAL_FOR_FORBIDDEN[request] : null;
|
|
837
|
+
if (forbiddenRefusal !== null) return {
|
|
838
|
+
kind: "check",
|
|
839
|
+
verdict: "refused",
|
|
840
|
+
detail: forbiddenRefusal
|
|
841
|
+
};
|
|
842
|
+
if (request === "read" && isHttpNotFound(error)) return {
|
|
843
|
+
kind: "check",
|
|
844
|
+
verdict: "refused",
|
|
845
|
+
detail: TRANSFORM_NOT_ON_INSTANCE
|
|
846
|
+
};
|
|
847
|
+
const failure = requestFailure(error);
|
|
848
|
+
if (failure === null) return null;
|
|
849
|
+
if (error instanceof NetworkError && request !== "run") return {
|
|
850
|
+
kind: "stop",
|
|
851
|
+
stop: {
|
|
852
|
+
code: failure.error_code,
|
|
853
|
+
message: failure.message
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
return {
|
|
857
|
+
kind: "check",
|
|
858
|
+
verdict: "error",
|
|
859
|
+
detail: failure
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/commands/ci/unit-stage.ts
|
|
864
|
+
const TEST_TIMEOUT_MS = 12e4;
|
|
865
|
+
const OK_STATUS = "ok";
|
|
866
|
+
const LOOKUP_KIND_BY_STATUS = /* @__PURE__ */ new Map([["not-found", "not-found"], ["invalid-format", "invalid-entity-id"]]);
|
|
867
|
+
function transformLookup(entityId, entry) {
|
|
868
|
+
if (entry?.status === OK_STATUS && entry.id !== void 0) return {
|
|
869
|
+
kind: "found",
|
|
870
|
+
id: entry.id
|
|
871
|
+
};
|
|
872
|
+
const kind = entry === void 0 ? void 0 : LOOKUP_KIND_BY_STATUS.get(entry.status);
|
|
873
|
+
if (kind === void 0) throw new InternalError(`eid translation answered ${entityId} with ${JSON.stringify(entry ?? null)}`);
|
|
874
|
+
return { kind };
|
|
875
|
+
}
|
|
876
|
+
function clientPort(client) {
|
|
877
|
+
return {
|
|
878
|
+
lookUpTransforms: async (entityIds) => {
|
|
879
|
+
const result = await client.eidTranslation.translate({ entity_ids: { transform: [...entityIds] } }, { signal: interruptSignal });
|
|
880
|
+
const lookups = /* @__PURE__ */ new Map();
|
|
881
|
+
for (const entityId of entityIds) lookups.set(entityId, transformLookup(entityId, result.entity_ids[entityId]));
|
|
882
|
+
return lookups;
|
|
883
|
+
},
|
|
884
|
+
readTransform: async (transformId) => {
|
|
885
|
+
await client.transform.get(transformId, { signal: interruptSignal });
|
|
886
|
+
},
|
|
887
|
+
listTests: async (transformId) => {
|
|
888
|
+
return (await client.transformTest.list({ "transform-id": transformId }, { signal: interruptSignal })).data;
|
|
889
|
+
},
|
|
890
|
+
runTest: (testId) => client.transformTest.run(testId, {
|
|
891
|
+
timeoutMs: TEST_TIMEOUT_MS,
|
|
892
|
+
signal: interruptSignal
|
|
893
|
+
}),
|
|
894
|
+
nowMs: () => performance.now()
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const MISSING_TRANSFORM_DETAIL = {
|
|
898
|
+
"not-found": TRANSFORM_NOT_ON_INSTANCE,
|
|
899
|
+
"invalid-entity-id": INVALID_ENTITY_ID
|
|
900
|
+
};
|
|
901
|
+
async function attempt(request, call) {
|
|
902
|
+
try {
|
|
903
|
+
return {
|
|
904
|
+
kind: "settled",
|
|
905
|
+
value: await call()
|
|
906
|
+
};
|
|
907
|
+
} catch (error) {
|
|
908
|
+
const failure = transformFailure(error, request);
|
|
909
|
+
if (failure === null) throw error;
|
|
910
|
+
return failure;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function reportFailure(failure, check, ms, report) {
|
|
914
|
+
if (failure.kind === "stop") return failure.stop;
|
|
915
|
+
report({
|
|
916
|
+
check,
|
|
917
|
+
verdict: failure.verdict,
|
|
918
|
+
warnings: [],
|
|
919
|
+
ms,
|
|
920
|
+
detail: failure.detail
|
|
921
|
+
});
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
async function checkTransform(transform, transformId, port, report) {
|
|
925
|
+
const transformCheck = {
|
|
926
|
+
kind: "transform-test",
|
|
927
|
+
id: transform.entity_id,
|
|
928
|
+
label: transform.name
|
|
929
|
+
};
|
|
930
|
+
const started = port.nowMs();
|
|
931
|
+
const read = await attempt("read", () => port.readTransform(transformId));
|
|
932
|
+
if (read.kind !== "settled") return reportFailure(read, transformCheck, port.nowMs() - started, report);
|
|
933
|
+
const tests = await attempt("list", () => port.listTests(transformId));
|
|
934
|
+
if (tests.kind !== "settled") return reportFailure(tests, transformCheck, port.nowMs() - started, report);
|
|
935
|
+
for (const test of tests.value.toSorted((a, b) => a.id - b.id)) {
|
|
936
|
+
const check = {
|
|
937
|
+
kind: "transform-test",
|
|
938
|
+
id: String(test.id),
|
|
939
|
+
label: `${transform.name}: ${test.name}`
|
|
940
|
+
};
|
|
941
|
+
const runStarted = port.nowMs();
|
|
942
|
+
const run = await attempt("run", () => port.runTest(test.id));
|
|
943
|
+
const ms = port.nowMs() - runStarted;
|
|
944
|
+
if (run.kind === "settled") {
|
|
945
|
+
report({
|
|
946
|
+
check,
|
|
947
|
+
verdict: testVerdict(run.value),
|
|
948
|
+
warnings: [],
|
|
949
|
+
ms,
|
|
950
|
+
detail: run.value
|
|
951
|
+
});
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const stop = reportFailure(run, check, ms, report);
|
|
955
|
+
if (stop !== null) return stop;
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
function unitStage(checked, connect) {
|
|
960
|
+
return {
|
|
961
|
+
name: "unit",
|
|
962
|
+
run: async (report) => {
|
|
963
|
+
const port = await connect();
|
|
964
|
+
let lookups;
|
|
965
|
+
try {
|
|
966
|
+
lookups = await port.lookUpTransforms(checked.map((transform) => transform.entity_id));
|
|
967
|
+
} catch (error) {
|
|
968
|
+
const stop = lookupStop(error);
|
|
969
|
+
if (stop === null) throw error;
|
|
970
|
+
return stop;
|
|
971
|
+
}
|
|
972
|
+
for (const transform of checked) {
|
|
973
|
+
const lookup = lookups.get(transform.entity_id);
|
|
974
|
+
if (lookup === void 0) throw new InternalError(`no lookup for transform ${transform.entity_id}`);
|
|
975
|
+
if (lookup.kind !== "found") {
|
|
976
|
+
report({
|
|
977
|
+
check: {
|
|
978
|
+
kind: "transform-test",
|
|
979
|
+
id: transform.entity_id,
|
|
980
|
+
label: transform.name
|
|
981
|
+
},
|
|
982
|
+
verdict: "refused",
|
|
983
|
+
warnings: [],
|
|
984
|
+
ms: 0,
|
|
985
|
+
detail: MISSING_TRANSFORM_DETAIL[lookup.kind]
|
|
986
|
+
});
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const stop = await checkTransform(transform, lookup.id, port, report);
|
|
990
|
+
if (stop !== null) return stop;
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/commands/ci/run.ts
|
|
998
|
+
var run_default = defineMetabaseCommand({
|
|
999
|
+
meta: {
|
|
1000
|
+
name: "run",
|
|
1001
|
+
description: "Check the transforms a branch changed, writing one JSON event per line"
|
|
1002
|
+
},
|
|
1003
|
+
details: "Compares --head with its merge base with --base and finds the transforms whose files or tests changed. Two stages check them: `lint` fails each transform the branch adds or whose logic (source or target) it edits that has no transform test, and warns about untested transforms the branch did not touch; `unit` runs the changed transforms' tests on the Metabase instance that --url, MB_URL or --profile names, in that order, which must already hold the branch's content, and is skipped when no transform changed. Content files are the `.yaml` files Metabase's import reads: not dotfiles, under a top-level serialization folder such as `transforms/` or `collections/`. Only committed content is checked: when --head is the checked-out commit, a content file with uncommitted changes is listed in an `uncommitted-content` warning, and its committed version is checked. Writes one JSON event per line (`job.start`, then each stage and check, then `job.end`); `job.start.checked` lists the transforms the job covered, and a `no-content-changes` warning says when the change touches no content file, so nothing was checked; each transform test's result is passed through as the check's `detail`. Exits 0 when the job passes. Exits 1 after `job.end` when it does not: `state` is `failed` when a check failed (fix the transform), and `errored` when no check failed but one could not judge the change (fix the test or the setup); a failure outranks errors, since it is a finding about the change. A test run that times out, loses its connection or gets an HTTP error is an `error` check whose `detail.error_code` starts with `ci.`, and the next test runs. A failure every later request would repeat ends the unit stage early, `errored` unless a check failed, with the reason in `stage.end.stopped`: Metabase rejects the credential, the server has no transform testing, the entity-id lookup fails, or Metabase cannot be reached before a test runs. Exits 2 on a usage or config error: a bad flag, a --repo, --base or --head that git cannot resolve or relate, or no Metabase credential when a transform needs testing, which is found after `job.start`. Exits 1 on a crash, such as a changed content file that does not parse, and 130 on an interrupt: the stream stops before `job.end`, the error is on stderr, and no --results file is left, not even an earlier run's. When the reader closes stdout early, the job still runs to the end, writes --results and exits by its state.",
|
|
1004
|
+
requires: [
|
|
1005
|
+
"eidTranslation.translate",
|
|
1006
|
+
"transform.get",
|
|
1007
|
+
"transformTest.list",
|
|
1008
|
+
"transformTest.run"
|
|
1009
|
+
],
|
|
1010
|
+
args: {
|
|
1011
|
+
...profileFlag,
|
|
1012
|
+
...connectionFlags,
|
|
1013
|
+
base: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
description: "Ref the branch is compared against",
|
|
1016
|
+
required: true
|
|
1017
|
+
},
|
|
1018
|
+
head: {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
description: "Ref to check",
|
|
1021
|
+
default: "HEAD"
|
|
1022
|
+
},
|
|
1023
|
+
repo: {
|
|
1024
|
+
type: "string",
|
|
1025
|
+
description: "Path inside the git work tree to check",
|
|
1026
|
+
default: "."
|
|
1027
|
+
},
|
|
1028
|
+
results: {
|
|
1029
|
+
type: "string",
|
|
1030
|
+
description: "Also write every event to this file, as one JSON array"
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
outputSchema: CiEvent,
|
|
1034
|
+
examples: ["mb ci run --base origin/main", "mb ci run --base origin/main --results results.json"],
|
|
1035
|
+
async run({ args, getClient }) {
|
|
1036
|
+
const state = await runJob({
|
|
1037
|
+
repo: args.repo,
|
|
1038
|
+
base: args.base,
|
|
1039
|
+
head: args.head,
|
|
1040
|
+
stages: async (plan) => {
|
|
1041
|
+
const lint = coverageStage(plan.coverage);
|
|
1042
|
+
if (plan.checked.length === 0) return [lint];
|
|
1043
|
+
return [lint, unitStage(plan.checked, async () => clientPort(await getClient()))];
|
|
1044
|
+
},
|
|
1045
|
+
job: randomUUID(),
|
|
1046
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
1047
|
+
write: jsonLineWriter(process.stdout),
|
|
1048
|
+
resultsPath: args.results ?? null,
|
|
1049
|
+
signal: interruptSignal
|
|
1050
|
+
});
|
|
1051
|
+
if (state !== "passed") throw new FailedResultError(`ci job ${state}`);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
//#endregion
|
|
1055
|
+
export { run_default as default };
|