@metabase/cli 0.5.7-alpha.bryan-rci-6-coverage.6f886fd → 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-C7c6Y3T3.mjs → add-collection-DIrpeq9O.mjs} +3 -3
- package/dist/{alert-J0qLs6rc.mjs → alert-DuUreVMq.mjs} +7 -7
- package/dist/{alerts-mjDIdof3.mjs → alerts-C7vJ-NAI.mjs} +4 -4
- package/dist/api-key-Cia6SHQ9.mjs +13 -0
- package/dist/{append-yaTEz2AE.mjs → append-v3fakJOd.mjs} +4 -4
- package/dist/{archive-Cg67G-qa.mjs → archive-5pjvTser.mjs} +3 -3
- package/dist/{archive-CgcNck0G.mjs → archive-BEhhmkTO.mjs} +3 -3
- package/dist/{archive-rhxBPOrL.mjs → archive-BrGj2yoP.mjs} +3 -3
- package/dist/{archive-B93SkIvs.mjs → archive-Bt8fwInI.mjs} +3 -3
- package/dist/{archive-VZ0dgYTd.mjs → archive-CmhV_hlP.mjs} +3 -3
- package/dist/{archive-DYdHrm6V.mjs → archive-CuygjxgQ.mjs} +3 -3
- package/dist/{archive-Dg9eMYU6.mjs → archive-Cy6RUJaD.mjs} +3 -3
- package/dist/{archive-CbtQNK96.mjs → archive-CyhH-395.mjs} +3 -3
- package/dist/{archive-DzXi2Frh.mjs → archive-D299FxXt.mjs} +3 -3
- package/dist/{archive-BmU6DnFz.mjs → archive-DdPaXaJo.mjs} +3 -3
- package/dist/{archive-DrCoSvio.mjs → archive-DwVbn3FG.mjs} +3 -3
- package/dist/{archive-P-y61Wi_.mjs → archive-TBPRGm7s.mjs} +3 -3
- package/dist/{auth-CxN5tDg1.mjs → auth-B63Hfuwg.mjs} +5 -5
- package/dist/{branches-1ZK62u6h.mjs → branches-BM0L124r.mjs} +2 -2
- package/dist/{breaking-Bv4wZN9X.mjs → breaking-VMUrgVVE.mjs} +3 -3
- package/dist/{broken-BU7MrnHI.mjs → broken-BbpNq5WG.mjs} +4 -4
- package/dist/{bulk-discard-values-6TLs4p2e.mjs → bulk-discard-values-Cohox-Zb.mjs} +4 -4
- package/dist/{bulk-edit-CYnFA1xm.mjs → bulk-edit-B9-XH592.mjs} +2 -2
- package/dist/{bulk-rescan-values-T__5XlPh.mjs → bulk-rescan-values-CsWKirZw.mjs} +3 -3
- package/dist/{bulk-sync-schema-V1Tpqvxg.mjs → bulk-sync-schema-CorpcAjz.mjs} +3 -3
- package/dist/{cancel-RTd-Bg5f.mjs → cancel-iGUVm1h2.mjs} +3 -3
- package/dist/{cancel-task-B6GpgrME.mjs → cancel-task-DWiicb_t.mjs} +2 -2
- package/dist/card-C-KsmoJW.mjs +25 -0
- package/dist/{cards-hYVyzntk.mjs → cards-D6jWz0N9.mjs} +3 -3
- package/dist/{ci-CPRKvyWe.mjs → ci-BQKTyiRi.mjs} +2 -2
- package/dist/cli.mjs +67 -46
- package/dist/{client-C_5K9lZA.mjs → client-Ba61gEcn.mjs} +6 -6
- package/dist/collection-DsNRlVcN.mjs +21 -0
- package/dist/{content-translation-Bux3aWnt.mjs → content-translation-B57aHSuc.mjs} +3 -3
- package/dist/{copy-B8HnAc11.mjs → copy-BeD_fk5J.mjs} +4 -4
- package/dist/{copy-BAP_y6s1.mjs → copy-D-VIVVB1.mjs} +4 -4
- package/dist/{copy-flags-VQzi7vqf.mjs → copy-flags-BcW9weVX.mjs} +1 -1
- package/dist/{create-CH2ebn9r.mjs → create-B--aPIbp.mjs} +4 -4
- package/dist/{create-BcbdVQr_.mjs → create-BURh8kHp.mjs} +3 -3
- package/dist/{create-Z3UZdGTY.mjs → create-BlmRvuQg.mjs} +3 -3
- package/dist/{create-BmEECHlW2.mjs → create-Bw9DAq_x2.mjs} +3 -3
- package/dist/{create-BWrIVbLC.mjs → create-C6c-clLK.mjs} +3 -3
- package/dist/{create-DF7jPhn1.mjs → create-CGiSOtIR.mjs} +3 -3
- package/dist/{create-DqtqJZa8.mjs → create-CP9lrYE4.mjs} +2 -2
- package/dist/{create-DNkqcne5.mjs → create-D-Eu1nBe.mjs} +2 -2
- package/dist/{create-BOn7IisH.mjs → create-D44fg_iT.mjs} +2 -2
- package/dist/{create-qCZ9z3II.mjs → create-DF7nC9qW.mjs} +2 -2
- package/dist/{create-CzVYAVbb.mjs → create-DMtlDzrE.mjs} +2 -2
- package/dist/{create-Bckej8a2.mjs → create-DQOapeeI.mjs} +2 -2
- package/dist/{create-BUPy6vmU.mjs → create-DcDyXL4g.mjs} +2 -2
- package/dist/{create-B0l5arU2.mjs → create-Dm8BvbsU.mjs} +2 -2
- package/dist/{create-BfD4GgYA.mjs → create-DybrZHV-.mjs} +2 -2
- package/dist/{create-Ds6gZXFm.mjs → create-HNchuWpj.mjs} +3 -3
- package/dist/{create-Dd_NNxYT.mjs → create-MmZVmRsH.mjs} +2 -2
- package/dist/{create-D70fh_7H.mjs → create-_wex54m1.mjs} +2 -2
- package/dist/{create-branch-CQyWpIxd.mjs → create-branch-C8arQT4M.mjs} +2 -2
- package/dist/{create-C_5-HH9u.mjs → create-lHqpAdxg.mjs} +3 -3
- package/dist/{create-CUcwiQgH.mjs → create-zALsdMhF.mjs} +2 -2
- package/dist/{csv-CFL145Ta.mjs → csv-CFabsH6l.mjs} +4 -4
- package/dist/{current-task-l8KZ0nv8.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-CxQNqhV2.mjs → data-app-DtW2i2SD.mjs} +10 -10
- package/dist/{data-app-project-B_zd-YrJ.mjs → data-app-project-CHYcJRrU.mjs} +1 -1
- package/dist/{db-DlDWN6UP.mjs → db-DSj2I1X9.mjs} +8 -8
- package/dist/{delete-3KVEIg6-.mjs → delete-AF64p0xG.mjs} +4 -4
- package/dist/{delete-YFXRPnnb.mjs → delete-C6N_Aj78.mjs} +4 -4
- package/dist/{delete-D3D0CTo-.mjs → delete-ClT1ROrM.mjs} +3 -3
- package/dist/{delete-CfkSkxHN.mjs → delete-CnGb4QIm.mjs} +4 -4
- package/dist/{delete-BmPR3qB0.mjs → delete-CsgkbmBy.mjs} +4 -4
- package/dist/{delete-Eje0xxEa.mjs → delete-Cx75BsN6.mjs} +4 -4
- package/dist/{delete-Dj0ImrZL.mjs → delete-CxL5okLN.mjs} +4 -4
- package/dist/{delete-BlLyxBY_.mjs → delete-D_l7FRE6.mjs} +4 -4
- package/dist/{delete-CcY_tcx4.mjs → delete-Dkwk2mZH.mjs} +4 -4
- package/dist/{delete-DweFD4-0.mjs → delete-YKX2HilI.mjs} +4 -4
- package/dist/{delete-runtime-C05-0fcn.mjs → delete-runtime-BUbOQx5I.mjs} +1 -1
- package/dist/{delete-table-BqpeGdMY.mjs → delete-table-CubRk-5H.mjs} +4 -4
- package/dist/{dependencies-DhA1jbDT.mjs → dependencies-W2dxsCpF.mjs} +3 -3
- package/dist/dependency-B0KmVz2u.mjs +15 -0
- package/dist/{dependents-4KgDZ4tl.mjs → dependents-B4V8pDYq.mjs} +4 -4
- package/dist/{dirty-DWwwmOmi.mjs → dirty-DKj4ScBN.mjs} +2 -2
- package/dist/{discard-values-6nmAd4kW.mjs → discard-values-5l91z3Dy.mjs} +4 -4
- package/dist/document--zYZP-u3.mjs +20 -0
- package/dist/{download-ZCfcKa2L.mjs → download-DRxR3yGi.mjs} +2 -2
- package/dist/{eid-CjLvyBgT.mjs → eid-B6JYogNC.mjs} +2 -2
- package/dist/{entry-flags-2u45mDJ4.mjs → entry-flags-6Y0fdYbG.mjs} +1 -1
- package/dist/{events-B_UxJw3Z.mjs → events-CE80dTib.mjs} +3 -3
- package/dist/{execute-Dg94sYQd.mjs → execute-C9foK2IU.mjs} +3 -3
- package/dist/{export-BdLB30iy.mjs → export-B8QyoPWP.mjs} +3 -3
- package/dist/{export-preflight-ChaWosZf.mjs → export-preflight-BQbwxyoJ.mjs} +2 -2
- package/dist/field-Bk09crKC.mjs +21 -0
- package/dist/{fields-B1cPeIGv.mjs → fields-TE3PnmwW.mjs} +3 -3
- package/dist/{filter-flags-BEz2s28-.mjs → filter-flags-DAUmdUwa.mjs} +1 -1
- package/dist/{fks-DgvW6D_7.mjs → fks-B41Re5dt.mjs} +3 -3
- package/dist/{get-C_bCGVsR.mjs → get-3XwqrjxC.mjs} +3 -3
- package/dist/{get-UNjwy8OI.mjs → get-BAGQ-bvt.mjs} +3 -3
- package/dist/{get-VrzY4qxg.mjs → get-BJgBsiEK.mjs} +2 -2
- package/dist/{get-BK7nxbPU.mjs → get-BcWr6U0n.mjs} +2 -2
- package/dist/{get-n9udbUQS.mjs → get-BrhIHZYg.mjs} +3 -3
- package/dist/{get-DStLX5Ah.mjs → get-BscyItLF.mjs} +3 -3
- package/dist/{get-B69NUYR0.mjs → get-CCTRO18v.mjs} +3 -3
- package/dist/{get-jphhROdg.mjs → get-CU6AWbqL.mjs} +3 -3
- package/dist/{get-CmWe9xq9.mjs → get-CefCrGWy.mjs} +5 -5
- package/dist/{get-Dsm_2AXn.mjs → get-Cjk-A9dQ.mjs} +3 -3
- package/dist/{get-DIR9LODa.mjs → get-Ck0cRNyr.mjs} +3 -3
- package/dist/{get-DC1lAsH_.mjs → get-CtRJUybv.mjs} +3 -3
- package/dist/{get-BxrS4Vi8.mjs → get-DG5fPl-S.mjs} +3 -3
- package/dist/{get-BYNEZFNj.mjs → get-DJeeXbwj.mjs} +3 -3
- package/dist/{get-DuVVF8xb.mjs → get-DbZDp2ve.mjs} +3 -3
- package/dist/{get-Ck2LNXxw.mjs → get-DeiC05Lm.mjs} +3 -3
- package/dist/{get-B94nj2uY.mjs → get-DguRhloC.mjs} +2 -2
- package/dist/{get-BM9AFJAi.mjs → get-DvPgnqAA.mjs} +2 -2
- package/dist/{get-Ljt0QVR0.mjs → get-PLIbg3Vy.mjs} +3 -3
- package/dist/{get-DkSvigdw.mjs → get-m-vpr7GN.mjs} +3 -3
- package/dist/{get-run-D5vfGnIN.mjs → get-run-DnKCnvUa.mjs} +3 -3
- package/dist/{get-D4oXmJTn.mjs → get-sjhoDJyB.mjs} +3 -3
- package/dist/{get-DckpwiZG.mjs → get-uWk0JKvF.mjs} +3 -3
- package/dist/git-sync-DMyH3Y85.mjs +29 -0
- package/dist/glossary-CcRGJaVi.mjs +14 -0
- package/dist/{graph-CvIhSOEe.mjs → graph-B2YkV2oX.mjs} +3 -3
- package/dist/{group-KiKh0UFs.mjs → group-D1nGaEXW.mjs} +1 -1
- package/dist/{has-remote-changes-57yJkr3b.mjs → has-remote-changes-ChMSjhmC.mjs} +2 -2
- package/dist/{import-HHsz_zDq.mjs → import-CNpN9iS0.mjs} +3 -3
- package/dist/{is-dirty-CxX_4rBC.mjs → is-dirty-Bud2wuxQ.mjs} +2 -2
- package/dist/{items-B2NR1_u3.mjs → items-B1DXzdwg.mjs} +2 -2
- package/dist/{library-DXvOVBTe.mjs → library-8G0s5VLO.mjs} +5 -5
- package/dist/{list-DEQvaioL.mjs → list-0aX638ov.mjs} +3 -3
- package/dist/{list-JS7ASJc-.mjs → list-B2PbnXp4.mjs} +2 -2
- package/dist/{list-Bwi5npRx.mjs → list-BJasizCe.mjs} +2 -2
- package/dist/{list-CcZAP1sH.mjs → list-BSvQ0AT_.mjs} +2 -2
- package/dist/{list-DhCSllG2.mjs → list-BVz0N0g_.mjs} +2 -2
- package/dist/{list-CtW9rAs4.mjs → list-BeaA7gQ7.mjs} +2 -2
- package/dist/{list-BU72dCx0.mjs → list-CAAvhl9h.mjs} +2 -2
- package/dist/{list-B44qweQa.mjs → list-CAZHyhd5.mjs} +2 -2
- package/dist/{list-l09h4Zp6.mjs → list-CO62N1eD.mjs} +2 -2
- package/dist/{list-Dv8U4e-C.mjs → list-CPwy-AEn.mjs} +2 -2
- package/dist/{list-CNoCRL4D.mjs → list-CcfGHN19.mjs} +2 -2
- package/dist/{list-rA9U_VyF.mjs → list-Ckqu41Zx.mjs} +2 -2
- package/dist/{list-DQ5BDB52.mjs → list-D1WO7lxP.mjs} +2 -2
- package/dist/{list-L4_At_o9.mjs → list-DAdWd8gn.mjs} +2 -2
- package/dist/{list-BsQHh8w4.mjs → list-DX5SmBCQ.mjs} +2 -2
- package/dist/{list-BK4VwtKe.mjs → list-DZLGnsAK.mjs} +2 -2
- package/dist/{list-DJogq3Ez.mjs → list-DjfY80vj.mjs} +2 -2
- package/dist/{list-BTpj7dv4.mjs → list-Dk3upD11.mjs} +3 -3
- package/dist/{list-DJ0_FcgB.mjs → list-DpyIV41y.mjs} +5 -5
- package/dist/{list-DiN5mPrk.mjs → list-JqIFAXpw.mjs} +2 -2
- package/dist/{list-DMbtH6tY.mjs → list-mRSKPKoO.mjs} +2 -2
- package/dist/{list-C6A-Eqgv.mjs → list-tHzgEkzv.mjs} +4 -4
- package/dist/{list-CY54buuD.mjs → list-z4iqffgl.mjs} +2 -2
- package/dist/{login-DVLN1666.mjs → login-muOdP15h.mjs} +5 -5
- package/dist/{logout-BiZxbftK.mjs → logout-Z0_gSSM4.mjs} +2 -2
- package/dist/{measure-B7Ud8GU1.mjs → measure-CTF4qqiE.mjs} +6 -6
- package/dist/{parameter-values-BwaIYczs.mjs → parameter-values-to5u_-2n.mjs} +3 -3
- package/dist/{parse-id-BXkvTl89.mjs → parse-id-CbF7zxGv.mjs} +1 -1
- package/dist/{path-BbR0QB1I.mjs → path-B-0S-9i2.mjs} +3 -3
- package/dist/{poll-D5eA9cN2.mjs → poll-zB3cX0Kq.mjs} +1 -1
- package/dist/{preflight-BJ5Bve-N.mjs → preflight-Dgj2yyY_.mjs} +1 -1
- package/dist/{process-cDa9fEBe.mjs → process-ByKCTa6A.mjs} +22 -10
- package/dist/{publish-d_WnLoJD.mjs → publish-tKh9tJeh.mjs} +3 -3
- package/dist/{query-Dkod9Tev.mjs → query-B4xK9j0u.mjs} +3 -3
- package/dist/{query-DFb-XQNz.mjs → query-o1Z2dgtw.mjs} +3 -3
- package/dist/{remapping-clW2Rgcz.mjs → remapping-DT305P-k.mjs} +3 -3
- package/dist/{remove-collection-CjRtrkLW.mjs → remove-collection-Bsme1OBA.mjs} +3 -3
- package/dist/{render-C_Oy6cF5.mjs → render-Df56xOPJ.mjs} +1 -1
- package/dist/{replace-8hEY7br8.mjs → replace-C3PQwvfl.mjs} +4 -4
- package/dist/{requirement-check-B7MNfiXv.mjs → requirement-check-qqRkRGcN.mjs} +1 -1
- package/dist/{rescan-values-0owEJyWj.mjs → rescan-values-C0P0upEB.mjs} +3 -3
- package/dist/{rescan-values-DaEu5sVZ.mjs → rescan-values-CGN4bjyB.mjs} +3 -3
- package/dist/{run-BlCJ0MYm.mjs → run-BaEBHum8.mjs} +4 -4
- package/dist/{run-Dc3u_olU.mjs → run-C_IZfmQ7.mjs} +403 -167
- package/dist/{run-Cr7QHB5S.mjs → run-ZRY5wy4x.mjs} +6 -6
- package/dist/{run-CXv_3NQt.mjs → run-bVD66LrU.mjs} +3 -3
- package/dist/{runs-624fiVP3.mjs → runs-DWm1pIdP.mjs} +3 -3
- package/dist/{runtime-DsfR_yrb.mjs → runtime-Dd2uBKqN.mjs} +3 -3
- package/dist/{scaffold-D4Jo6VQc.mjs → scaffold-BnT99dhY.mjs} +4 -4
- package/dist/{schema-DXsx2_Mk.mjs → schema-DoELV3FT.mjs} +2 -2
- package/dist/{schema-tables-Bat1bYLS.mjs → schema-tables-DGyHJnJX.mjs} +3 -3
- package/dist/{schemas-B8bgnxOA.mjs → schemas-7w9MPZcZ.mjs} +3 -3
- package/dist/{search-fXh_MkLZ.mjs → search-BZh0BwZ9.mjs} +3 -3
- package/dist/{search-BslDGFtS.mjs → search-CyyySVTz.mjs} +3 -3
- package/dist/{segment-Bb7JTPmv.mjs → segment--JCnZCgL.mjs} +6 -6
- package/dist/{send-YnL6iiOt.mjs → send-c4FpapMf.mjs} +3 -3
- package/dist/{set-CydWEHNW.mjs → set-_puxfyaP.mjs} +2 -2
- package/dist/{set-active-C-x_A0AY.mjs → set-active-BexM75_s.mjs} +2 -2
- package/dist/{set-bundle-DaHlkEYA.mjs → set-bundle-B-qAvkDo.mjs} +3 -3
- package/dist/{set-data-actions-C3NwuCSF.mjs → set-data-actions-BnrX6ARh.mjs} +3 -3
- package/dist/{set-enabled-CjUOudzn.mjs → set-enabled-Np4xqFA_.mjs} +2 -2
- package/dist/{set-sensitivity-DAodU0MG.mjs → set-sensitivity-DDzrkjdv.mjs} +3 -3
- package/dist/{setting-Bil7qFf0.mjs → setting-BGK1uRxm.mjs} +4 -4
- package/dist/{setup-B8GRygkY.mjs → setup-BCliYRAt.mjs} +2 -2
- package/dist/{skills-CW1wIl0c.mjs → skills-CoXIflBw.mjs} +3 -3
- package/dist/{skills-DYfZF1GA.mjs → skills-PULUh2VV.mjs} +3 -3
- package/dist/snippet-Czzma-M2.mjs +19 -0
- package/dist/{stash-T0WRHnFD.mjs → stash-d3FtnE2F.mjs} +3 -3
- package/dist/{status-BRMzSa12.mjs → status-BJTdBEN4.mjs} +3 -3
- package/dist/{status-CW1bsfaP.mjs → status-CJLd6Uk6.mjs} +2 -2
- package/dist/{subscription-Edc8UK2V.mjs → subscription-Dpq45fMP.mjs} +6 -6
- package/dist/{subscriptions-DMiWl84r.mjs → subscriptions-Cu4km2CN.mjs} +4 -4
- package/dist/{summary-CFnwsMoV.mjs → summary-3Av5251t.mjs} +3 -3
- package/dist/{sync-schema-COvbx-g4.mjs → sync-schema-BNHt8S3P.mjs} +4 -4
- package/dist/{sync-schema-VzwA_5tH.mjs → sync-schema-BYJuUBsG.mjs} +3 -3
- package/dist/table-CwfF6ZTZ.mjs +26 -0
- package/dist/{table-selector-flags-BhZo8RHH.mjs → table-selector-flags-BiA8p56s.mjs} +2 -2
- package/dist/timeline-C4WCMzDX.mjs +21 -0
- package/dist/{timeline-event-BqM82_7L.mjs → timeline-event-BLBqSAjH.mjs} +6 -6
- package/dist/transform-job-SQ57QOlQ.mjs +22 -0
- package/dist/transform-jxF8cjNA.mjs +28 -0
- package/dist/{transform-tag-CxXDADhM.mjs → transform-tag-CETHXuAi.mjs} +5 -5
- package/dist/{transform-test-C0c4zjcf.mjs → transform-test-D-e9oi6r.mjs} +1 -1
- package/dist/{transform-test-DHTxBt4k.mjs → transform-test-D6QIyu8C.mjs} +6 -2
- package/dist/transform-test-MlVjTlKo.mjs +16 -0
- package/dist/{transforms-WyRUDM1j.mjs → transforms-CstSQgQ-.mjs} +3 -3
- package/dist/{transport-Byycx3RW.mjs → transport-D74axlBJ.mjs} +3 -3
- package/dist/{tree-CMeV4bI8.mjs → tree-BDhm4E_W.mjs} +2 -2
- package/dist/{typed-schema-DsGXH-La.mjs → typed-schema-BZkv_z2f.mjs} +3 -3
- package/dist/{unpublish-EaVPnnZ5.mjs → unpublish-BNBSxZFl.mjs} +3 -3
- package/dist/{unreferenced-BvE7ke0M.mjs → unreferenced-B2nDcQ7m.mjs} +3 -3
- package/dist/{update-BtWR4ri9.mjs → update-B6xWuJqk.mjs} +3 -3
- package/dist/{update-DrY4i6eO.mjs → update-B9iHHoJB.mjs} +4 -4
- package/dist/{update-DZPo69an.mjs → update-BA0NxInb.mjs} +3 -3
- package/dist/{update-DyXj5mTv.mjs → update-BGFGng3a.mjs} +4 -4
- package/dist/{update-Dcwpy8BO.mjs → update-BMHPp3nt.mjs} +3 -3
- package/dist/{update-Bb1HPTH2.mjs → update-BS-E7QLh.mjs} +3 -3
- package/dist/{update-DwGo58Kc.mjs → update-BenGrugz.mjs} +4 -4
- package/dist/{update--uKYjq9_.mjs → update-BkFy2BV_.mjs} +3 -3
- package/dist/{update-BSWoKNrf.mjs → update-ByBqo1if.mjs} +4 -4
- package/dist/{update-CFYVJJqw.mjs → update-CFLJWHOT.mjs} +3 -3
- package/dist/{update-DjBJQCNg.mjs → update-CWzmrGS_.mjs} +3 -3
- package/dist/{update-BDghcetp.mjs → update-Cpd1tKqr.mjs} +3 -3
- package/dist/{update-CNzu9eG1.mjs → update-DKcPzoG9.mjs} +5 -5
- package/dist/{update-B8EnadKJ.mjs → update-DRw_610p.mjs} +4 -4
- package/dist/{update-t2zdccd5.mjs → update-DT8KwBxO.mjs} +4 -4
- package/dist/{update-CT9e8m-I.mjs → update-DU3jpGps.mjs} +3 -3
- package/dist/{update-BPhLIVs9.mjs → update-DrfAOlkf.mjs} +2 -2
- package/dist/{update-wh2EjN242.mjs → update-DrvDC_0m2.mjs} +3 -3
- package/dist/{update-BgQ9TU63.mjs → update-DxLypq9G.mjs} +4 -4
- package/dist/{update-dashcard-BhT2FBd4.mjs → update-dashcard-BU3Wqcnf.mjs} +3 -3
- package/dist/{update-xYxKw2UB.mjs → update-pzu78yZB.mjs} +3 -3
- package/dist/{upgrade-BqBjnjjU.mjs → upgrade-Cq89D4Oe.mjs} +3 -3
- package/dist/{upload-D0zRTkyv.mjs → upload-CmwO6anj.mjs} +1 -1
- package/dist/upload-DbDFl9qT.mjs +13 -0
- package/dist/{upload-kM0-7Bx2.mjs → upload-INQHve86.mjs} +3 -3
- package/dist/{uuid-DsuYe9z7.mjs → uuid-BNJKaQGB.mjs} +2 -2
- package/dist/{validate-query-Bf5FhhMI.mjs → validate-query-CQ4UPpZX.mjs} +1 -1
- package/dist/{values-kLEvyjSy.mjs → values-BVtqwRlR.mjs} +3 -3
- package/dist/{verify-CvYsiaqY.mjs → verify-BSlj7S_E.mjs} +3 -3
- package/dist/{verify-ChvmI4lq.mjs → verify-Bp9ZyPgX.mjs} +2 -2
- package/dist/{verify-D27n5y3t.mjs → verify-BwodYH0C.mjs} +3 -3
- package/dist/{wait-DioDMGLJ.mjs → wait-DZclBI3D.mjs} +4 -4
- package/dist/{wait-flags-Qqvlb3Rv.mjs → wait-flags-CmIfm6_U.mjs} +2 -2
- package/dist/{yaml-QoEP-7KU.mjs → yaml-BthVUNj3.mjs} +16 -1
- 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-Bw2jDvT4.mjs +0 -13
- package/dist/card-BlSUKf2e.mjs +0 -25
- package/dist/collection-CeaSalia.mjs +0 -21
- package/dist/dashboard-CIOXUaBJ.mjs +0 -35
- package/dist/data-action-_7UX21i9.mjs +0 -24
- package/dist/dependency-BPwCXzX4.mjs +0 -15
- package/dist/document-CC60L29U.mjs +0 -20
- package/dist/field-ClWwbYtG.mjs +0 -21
- package/dist/git-sync-Bc9pf1LL.mjs +0 -29
- package/dist/glossary-CAzA-OIR.mjs +0 -14
- package/dist/snippet-D9g83-mO.mjs +0 -19
- package/dist/table-CC-rFakp.mjs +0 -26
- package/dist/timeline-C2ZUL_9F.mjs +0 -21
- package/dist/transform-BtB6iOJ9.mjs +0 -28
- package/dist/transform-job-I2Q5f0yN.mjs +0 -22
- package/dist/transform-test-B6jvvXeh.mjs +0 -16
- package/dist/upload-C0GfBUvp.mjs +0 -13
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { c as InternalError, h as ValidationError, o as ConfigError, r as isPlainObject, s as FailedResultError } from "./predicates-0C0TsWcJ.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { t as defineMetabaseCommand } from "./runtime-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { t as
|
|
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
7
|
import { z } from "zod";
|
|
8
|
-
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { isDeepStrictEqual } from "node:util";
|
|
11
12
|
import { performance } from "node:perf_hooks";
|
|
@@ -82,6 +83,7 @@ function coverageStage(assessment) {
|
|
|
82
83
|
name: transform.name
|
|
83
84
|
})) }
|
|
84
85
|
});
|
|
86
|
+
return null;
|
|
85
87
|
}
|
|
86
88
|
};
|
|
87
89
|
}
|
|
@@ -103,6 +105,10 @@ const CiWarning = z.object({
|
|
|
103
105
|
code: z.string(),
|
|
104
106
|
message: z.string()
|
|
105
107
|
});
|
|
108
|
+
const StageStop = z.object({
|
|
109
|
+
code: z.string(),
|
|
110
|
+
message: z.string()
|
|
111
|
+
});
|
|
106
112
|
const CheckedTransform = z.object({
|
|
107
113
|
entity_id: z.string(),
|
|
108
114
|
name: z.string()
|
|
@@ -150,7 +156,8 @@ const CheckResult = EventEnvelope.extend({
|
|
|
150
156
|
const StageEnd = EventEnvelope.extend({
|
|
151
157
|
type: z.literal("stage.end"),
|
|
152
158
|
stage: StageName,
|
|
153
|
-
status: StageStatus
|
|
159
|
+
status: StageStatus,
|
|
160
|
+
stopped: StageStop.nullable()
|
|
154
161
|
});
|
|
155
162
|
const JobEnd = EventEnvelope.extend({
|
|
156
163
|
type: z.literal("job.end"),
|
|
@@ -179,11 +186,12 @@ function createEmitter(options) {
|
|
|
179
186
|
if (!parsed.success) throw new InternalError(`ci event "${body.type}" does not match its schema: ${parsed.error.message}`);
|
|
180
187
|
seq += 1;
|
|
181
188
|
options.write(parsed.data);
|
|
189
|
+
return parsed.data;
|
|
182
190
|
};
|
|
183
191
|
}
|
|
184
|
-
function stageStatus(verdicts) {
|
|
192
|
+
function stageStatus(verdicts, stopped) {
|
|
185
193
|
if (verdicts.includes("failed")) return "failed";
|
|
186
|
-
if (verdicts.includes("refused") || verdicts.includes("error")) return "errored";
|
|
194
|
+
if (stopped !== null || verdicts.includes("refused") || verdicts.includes("error")) return "errored";
|
|
187
195
|
return "passed";
|
|
188
196
|
}
|
|
189
197
|
async function runStages(stages, emit) {
|
|
@@ -195,7 +203,8 @@ async function runStages(stages, emit) {
|
|
|
195
203
|
emit({
|
|
196
204
|
type: "stage.end",
|
|
197
205
|
stage: stage.name,
|
|
198
|
-
status: "skipped"
|
|
206
|
+
status: "skipped",
|
|
207
|
+
stopped: null
|
|
199
208
|
});
|
|
200
209
|
continue;
|
|
201
210
|
}
|
|
@@ -204,7 +213,7 @@ async function runStages(stages, emit) {
|
|
|
204
213
|
stage: stage.name
|
|
205
214
|
});
|
|
206
215
|
const verdicts = [];
|
|
207
|
-
await stage.run((check) => {
|
|
216
|
+
const stopped = await stage.run((check) => {
|
|
208
217
|
verdicts.push(check.verdict);
|
|
209
218
|
checks += 1;
|
|
210
219
|
warnings += check.warnings.length;
|
|
@@ -214,11 +223,12 @@ async function runStages(stages, emit) {
|
|
|
214
223
|
...check
|
|
215
224
|
});
|
|
216
225
|
});
|
|
217
|
-
state = stageStatus(verdicts);
|
|
226
|
+
state = stageStatus(verdicts, stopped);
|
|
218
227
|
emit({
|
|
219
228
|
type: "stage.end",
|
|
220
229
|
stage: stage.name,
|
|
221
|
-
status: state
|
|
230
|
+
status: state,
|
|
231
|
+
stopped
|
|
222
232
|
});
|
|
223
233
|
}
|
|
224
234
|
return {
|
|
@@ -235,6 +245,7 @@ const SINGLE_PATH_STATUSES = /* @__PURE__ */ new Map([
|
|
|
235
245
|
["D", "deleted"],
|
|
236
246
|
["T", "modified"]
|
|
237
247
|
]);
|
|
248
|
+
const STATUS_CODE = /^(?:[AMDT](?:\d{3})?|[RC]\d{3})$/;
|
|
238
249
|
const RENAME_STATUS = "R";
|
|
239
250
|
const COPY_STATUS = "C";
|
|
240
251
|
function nameStatusError(message) {
|
|
@@ -252,6 +263,7 @@ function parseNameStatus(output) {
|
|
|
252
263
|
let index = 0;
|
|
253
264
|
while (index < tokens.length) {
|
|
254
265
|
const code = requireToken(tokens[index], "a status");
|
|
266
|
+
if (!STATUS_CODE.test(code)) throw nameStatusError(`unexpected status "${code}"`);
|
|
255
267
|
const missingPath = `a path for status "${code}"`;
|
|
256
268
|
const first = tokens[index + 1];
|
|
257
269
|
const second = tokens[index + 2];
|
|
@@ -275,6 +287,7 @@ function parseNameStatus(output) {
|
|
|
275
287
|
continue;
|
|
276
288
|
}
|
|
277
289
|
if (letter === COPY_STATUS) {
|
|
290
|
+
requireToken(first, missingPath);
|
|
278
291
|
entries.push({
|
|
279
292
|
status: "added",
|
|
280
293
|
path: requireToken(second, missingPath)
|
|
@@ -315,46 +328,34 @@ const TransformDocument = z.object({
|
|
|
315
328
|
}).loose();
|
|
316
329
|
const TransformTestDocument = z.object({ transform_id: z.string().min(1) }).loose();
|
|
317
330
|
const SerdesMeta = z.array(z.object({ model: z.string() }).loose()).min(1);
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!meta.success) throw new ValidationError(`${path}: ${SERDES_META_KEY} must be a non-empty list of entries with a model`, {
|
|
331
|
+
function parseOrRefuse(path, schema, value, rule) {
|
|
332
|
+
const parsed = schema.safeParse(value);
|
|
333
|
+
if (!parsed.success) throw new ValidationError(`${path}: ${rule}`, {
|
|
322
334
|
source: path,
|
|
323
|
-
zodIssues:
|
|
335
|
+
zodIssues: parsed.error.issues
|
|
324
336
|
});
|
|
325
|
-
|
|
326
|
-
if (last === void 0) throw new InternalError(`${path}: ${SERDES_META_KEY} passed a non-empty check but has no entries`);
|
|
327
|
-
return last.model;
|
|
337
|
+
return parsed.data;
|
|
328
338
|
}
|
|
329
339
|
function classifyContentFile(path, document) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
zodIssues: transform.error.issues
|
|
336
|
-
});
|
|
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");
|
|
337
345
|
return {
|
|
338
346
|
kind: "transform",
|
|
339
|
-
entityId: transform.
|
|
340
|
-
name: transform.
|
|
347
|
+
entityId: transform.entity_id,
|
|
348
|
+
name: transform.name,
|
|
341
349
|
logic: {
|
|
342
|
-
source: transform.
|
|
343
|
-
target: transform.
|
|
350
|
+
source: transform.source,
|
|
351
|
+
target: transform.target
|
|
344
352
|
}
|
|
345
353
|
};
|
|
346
354
|
}
|
|
347
|
-
if (model === "TransformTest") {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
zodIssues: test.error.issues
|
|
352
|
-
});
|
|
353
|
-
return {
|
|
354
|
-
kind: "transform-test",
|
|
355
|
-
transformId: test.data.transform_id
|
|
356
|
-
};
|
|
357
|
-
}
|
|
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
|
+
};
|
|
358
359
|
return { kind: "other" };
|
|
359
360
|
}
|
|
360
361
|
function transformEntityId(file) {
|
|
@@ -364,7 +365,11 @@ function affectedTransforms(changes) {
|
|
|
364
365
|
const touched = /* @__PURE__ */ new Set();
|
|
365
366
|
const atHead = /* @__PURE__ */ new Set();
|
|
366
367
|
const atBase = /* @__PURE__ */ new Set();
|
|
367
|
-
for (const { base, head } of changes) {
|
|
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
|
+
});
|
|
368
373
|
const headId = transformEntityId(head);
|
|
369
374
|
const baseId = transformEntityId(base);
|
|
370
375
|
if (headId !== null) {
|
|
@@ -376,16 +381,56 @@ function affectedTransforms(changes) {
|
|
|
376
381
|
}
|
|
377
382
|
return [...touched].filter((entityId) => atHead.has(entityId) || !atBase.has(entityId));
|
|
378
383
|
}
|
|
379
|
-
const
|
|
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);
|
|
380
412
|
function isContentPath(path) {
|
|
381
|
-
|
|
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
|
+
};
|
|
382
427
|
}
|
|
383
428
|
function uncommittedContentWarning(paths) {
|
|
384
429
|
const content = paths.filter(isContentPath);
|
|
385
430
|
if (content.length === 0) return null;
|
|
386
431
|
return {
|
|
387
432
|
code: "uncommitted-content",
|
|
388
|
-
message: `${content.length === 1 ? "1 content file
|
|
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(", ")}`
|
|
389
434
|
};
|
|
390
435
|
}
|
|
391
436
|
//#endregion
|
|
@@ -394,65 +439,71 @@ const GIT = "git";
|
|
|
394
439
|
const NO_MATCH_EXIT_CODE = 1;
|
|
395
440
|
const NO_MERGE_BASE_EXIT_CODE = 1;
|
|
396
441
|
const UNKNOWN_REF_EXIT_CODE = 1;
|
|
397
|
-
|
|
442
|
+
const GIT_TIMEOUT_MS = 12e4;
|
|
443
|
+
async function withGit(repo, args, spawn) {
|
|
398
444
|
try {
|
|
399
445
|
return await spawn();
|
|
400
446
|
} catch (error) {
|
|
401
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}`);
|
|
402
450
|
throw error;
|
|
403
451
|
}
|
|
404
452
|
}
|
|
405
|
-
function runGit(repo, args) {
|
|
406
|
-
return withGit(() => runProcess(GIT, [
|
|
453
|
+
function runGit(repo, args, signal) {
|
|
454
|
+
return withGit(repo, args, () => runProcess(GIT, [
|
|
407
455
|
"-C",
|
|
408
456
|
repo,
|
|
409
457
|
...args
|
|
410
|
-
]
|
|
458
|
+
], {
|
|
459
|
+
signal,
|
|
460
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
461
|
+
}));
|
|
411
462
|
}
|
|
412
|
-
async function git(repo, args) {
|
|
413
|
-
const result = await runGit(repo, args);
|
|
463
|
+
async function git(repo, args, signal) {
|
|
464
|
+
const result = await runGit(repo, args, signal);
|
|
414
465
|
if (result.exitCode !== 0) throw new ConfigError(`git ${args.join(" ")} failed in ${repo}: ${result.stderr.trim()}`);
|
|
415
466
|
return result.stdout;
|
|
416
467
|
}
|
|
417
|
-
async function repositoryRoot(repo) {
|
|
418
|
-
const result = await runGit(repo, ["rev-parse", "--show-toplevel"]);
|
|
468
|
+
async function repositoryRoot(repo, signal) {
|
|
469
|
+
const result = await runGit(repo, ["rev-parse", "--show-toplevel"], signal);
|
|
419
470
|
if (result.exitCode !== 0) throw new ConfigError(`--repo ${repo} is not inside a git work tree: ${result.stderr.trim()}`);
|
|
420
471
|
return result.stdout.trim();
|
|
421
472
|
}
|
|
422
|
-
async function resolveCommit(repo, ref, flag) {
|
|
473
|
+
async function resolveCommit(repo, ref, flag, signal) {
|
|
423
474
|
const result = await runGit(repo, [
|
|
424
475
|
"rev-parse",
|
|
425
476
|
"--verify",
|
|
426
477
|
"--quiet",
|
|
427
478
|
"--end-of-options",
|
|
428
479
|
`${ref}^{commit}`
|
|
429
|
-
]);
|
|
480
|
+
], signal);
|
|
430
481
|
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) throw new ConfigError(`${flag} ${ref} is not a commit in ${repo}`);
|
|
431
482
|
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse failed in ${repo}: ${result.stderr.trim()}`);
|
|
432
483
|
return result.stdout.trim();
|
|
433
484
|
}
|
|
434
|
-
async function checkedOutCommit(repo) {
|
|
485
|
+
async function checkedOutCommit(repo, signal) {
|
|
435
486
|
const result = await runGit(repo, [
|
|
436
487
|
"rev-parse",
|
|
437
488
|
"--verify",
|
|
438
489
|
"--quiet",
|
|
439
490
|
"HEAD"
|
|
440
|
-
]);
|
|
491
|
+
], signal);
|
|
441
492
|
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) return null;
|
|
442
493
|
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse HEAD failed in ${repo}: ${result.stderr.trim()}`);
|
|
443
494
|
return result.stdout.trim();
|
|
444
495
|
}
|
|
445
|
-
async function mergeBase(repo, base, head) {
|
|
496
|
+
async function mergeBase(repo, base, head, signal) {
|
|
446
497
|
const result = await runGit(repo, [
|
|
447
498
|
"merge-base",
|
|
448
499
|
base,
|
|
449
500
|
head
|
|
450
|
-
]);
|
|
501
|
+
], signal);
|
|
451
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`);
|
|
452
503
|
if (result.exitCode !== 0) throw new ConfigError(`git merge-base failed in ${repo}: ${result.stderr.trim()}`);
|
|
453
504
|
return result.stdout.trim();
|
|
454
505
|
}
|
|
455
|
-
function diffNameStatus(repo, from, to) {
|
|
506
|
+
function diffNameStatus(repo, from, to, signal) {
|
|
456
507
|
return git(repo, [
|
|
457
508
|
"diff",
|
|
458
509
|
"--name-status",
|
|
@@ -460,24 +511,35 @@ function diffNameStatus(repo, from, to) {
|
|
|
460
511
|
"-M",
|
|
461
512
|
from,
|
|
462
513
|
to
|
|
463
|
-
]);
|
|
514
|
+
], signal);
|
|
464
515
|
}
|
|
465
|
-
function statusPorcelain(repo) {
|
|
516
|
+
function statusPorcelain(repo, signal) {
|
|
466
517
|
return git(repo, [
|
|
467
518
|
"status",
|
|
468
519
|
"--porcelain",
|
|
469
520
|
"-z",
|
|
470
521
|
"--untracked-files=all"
|
|
471
|
-
]);
|
|
522
|
+
], signal);
|
|
472
523
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
"
|
|
476
|
-
"
|
|
477
|
-
|
|
478
|
-
|
|
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;
|
|
479
541
|
}
|
|
480
|
-
async function filesMatching(repo, commit, pattern, pathspecs) {
|
|
542
|
+
async function filesMatching(repo, commit, pattern, pathspecs, signal) {
|
|
481
543
|
const result = await runGit(repo, [
|
|
482
544
|
"grep",
|
|
483
545
|
"--no-color",
|
|
@@ -489,7 +551,7 @@ async function filesMatching(repo, commit, pattern, pathspecs) {
|
|
|
489
551
|
commit,
|
|
490
552
|
"--",
|
|
491
553
|
...pathspecs
|
|
492
|
-
]);
|
|
554
|
+
], signal);
|
|
493
555
|
if (result.exitCode === NO_MATCH_EXIT_CODE) return [];
|
|
494
556
|
if (result.exitCode !== 0) throw new ConfigError(`git grep failed in ${repo}: ${result.stderr.trim()}`);
|
|
495
557
|
const prefix = `${commit}:`;
|
|
@@ -500,17 +562,21 @@ async function filesMatching(repo, commit, pattern, pathspecs) {
|
|
|
500
562
|
}
|
|
501
563
|
const NEWLINE = 10;
|
|
502
564
|
const utf8 = new TextDecoder("utf-8");
|
|
503
|
-
async function readFilesAt(repo, commit, paths) {
|
|
565
|
+
async function readFilesAt(repo, commit, paths, signal) {
|
|
504
566
|
const files = /* @__PURE__ */ new Map();
|
|
505
567
|
if (paths.length === 0) return files;
|
|
506
568
|
const multiline = paths.find((path) => path.includes("\n"));
|
|
507
569
|
if (multiline !== void 0) throw new ConfigError(`cannot read ${JSON.stringify(multiline)}: a path with a newline cannot be batched`);
|
|
508
|
-
const
|
|
570
|
+
const args = ["cat-file", "--batch"];
|
|
571
|
+
const result = await withGit(repo, args, () => runProcessBinary(GIT, [
|
|
509
572
|
"-C",
|
|
510
573
|
repo,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
574
|
+
...args
|
|
575
|
+
], {
|
|
576
|
+
stdin: paths.map((path) => `${commit}:${path}\n`).join(""),
|
|
577
|
+
signal,
|
|
578
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
579
|
+
}));
|
|
514
580
|
if (result.exitCode !== 0) throw new ConfigError(`git cat-file --batch failed in ${repo}: ${result.stderr.trim()}`);
|
|
515
581
|
const output = result.stdout;
|
|
516
582
|
let offset = 0;
|
|
@@ -530,10 +596,23 @@ async function readFilesAt(repo, commit, paths) {
|
|
|
530
596
|
}
|
|
531
597
|
//#endregion
|
|
532
598
|
//#region src/commands/ci/job-plan.ts
|
|
533
|
-
const CONTENT_PATHSPECS =
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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;
|
|
537
616
|
}
|
|
538
617
|
function basePath(entry) {
|
|
539
618
|
if (entry.status === "added") return null;
|
|
@@ -542,41 +621,42 @@ function basePath(entry) {
|
|
|
542
621
|
function headPath(entry) {
|
|
543
622
|
return entry.status === "deleted" ? null : entry.path;
|
|
544
623
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
base: baseSide,
|
|
549
|
-
head: headSide
|
|
550
|
-
};
|
|
624
|
+
function present(paths) {
|
|
625
|
+
return paths.filter((path) => path !== null);
|
|
551
626
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
556
632
|
}
|
|
557
|
-
async function
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
for (const [path, text] of await readFilesAt(repo, head, unchanged)) record(atHead, classifyContentFile(path, parseYaml(text, z.unknown(), { source: `${head}:${path}` })));
|
|
565
|
-
return atHead;
|
|
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
|
+
}));
|
|
566
640
|
}
|
|
567
|
-
async function
|
|
568
|
-
const
|
|
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);
|
|
569
647
|
const [base, head, checkout] = await Promise.all([
|
|
570
|
-
resolveCommit(repo, baseRef, "--base"),
|
|
571
|
-
resolveCommit(repo, headRef, "--head"),
|
|
572
|
-
checkedOutCommit(repo)
|
|
648
|
+
resolveCommit(repo, baseRef, "--base", signal),
|
|
649
|
+
resolveCommit(repo, headRef, "--head", signal),
|
|
650
|
+
checkedOutCommit(repo, signal)
|
|
573
651
|
]);
|
|
574
|
-
const forkPoint = await mergeBase(repo, base, head);
|
|
575
|
-
const entries = parseNameStatus(await diffNameStatus(repo, forkPoint, head));
|
|
576
|
-
const changes = await
|
|
577
|
-
const atHead = await scanHead(repo, head, changes, new Set(entries.map((entry) => entry.path)));
|
|
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);
|
|
578
656
|
const checked = [];
|
|
579
657
|
const warnings = [];
|
|
658
|
+
const noContent = noContentChangesWarning(entries);
|
|
659
|
+
if (noContent !== null) warnings.push(noContent);
|
|
580
660
|
for (const entityId of affectedTransforms(changes)) {
|
|
581
661
|
const name = atHead.names.get(entityId);
|
|
582
662
|
if (name === void 0) {
|
|
@@ -597,7 +677,7 @@ async function planJob(repoPath, baseRef, headRef) {
|
|
|
597
677
|
testedAtHead: atHead.tested
|
|
598
678
|
});
|
|
599
679
|
if (head === checkout) {
|
|
600
|
-
const uncommitted = uncommittedContentWarning(parseStatusPorcelain(await statusPorcelain(repo)));
|
|
680
|
+
const uncommitted = uncommittedContentWarning(parseStatusPorcelain(await statusPorcelain(repo, signal)));
|
|
601
681
|
if (uncommitted !== null) warnings.push(uncommitted);
|
|
602
682
|
}
|
|
603
683
|
return {
|
|
@@ -612,17 +692,31 @@ async function planJob(repoPath, baseRef, headRef) {
|
|
|
612
692
|
}
|
|
613
693
|
//#endregion
|
|
614
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
|
+
}
|
|
615
708
|
async function runJob(options) {
|
|
709
|
+
if (options.resultsPath !== null) await rm(options.resultsPath, { force: true });
|
|
616
710
|
const events = [];
|
|
617
711
|
const emit = createEmitter({
|
|
618
712
|
job: options.job,
|
|
619
713
|
now: options.now,
|
|
620
714
|
write: (event) => {
|
|
621
715
|
events.push(event);
|
|
622
|
-
options.write(event);
|
|
716
|
+
if (event.type !== "job.end") options.write(event);
|
|
623
717
|
}
|
|
624
718
|
});
|
|
625
|
-
const plan = await planJob(options.repo, options.base, options.head);
|
|
719
|
+
const plan = await planJob(options.repo, options.base, options.head, options.signal);
|
|
626
720
|
emit({
|
|
627
721
|
type: "job.start",
|
|
628
722
|
v: 1,
|
|
@@ -631,7 +725,7 @@ async function runJob(options) {
|
|
|
631
725
|
warnings: plan.warnings
|
|
632
726
|
});
|
|
633
727
|
const outcome = await runStages(await options.stages(plan), emit);
|
|
634
|
-
emit({
|
|
728
|
+
const end = emit({
|
|
635
729
|
type: "job.end",
|
|
636
730
|
state: outcome.state,
|
|
637
731
|
counts: {
|
|
@@ -639,7 +733,8 @@ async function runJob(options) {
|
|
|
639
733
|
warnings: outcome.warnings + plan.warnings.length
|
|
640
734
|
}
|
|
641
735
|
});
|
|
642
|
-
if (options.resultsPath !== null) await
|
|
736
|
+
if (options.resultsPath !== null) await writeResults(options.resultsPath, events);
|
|
737
|
+
options.write(end);
|
|
643
738
|
return outcome.state;
|
|
644
739
|
}
|
|
645
740
|
//#endregion
|
|
@@ -648,21 +743,122 @@ function testVerdict(result) {
|
|
|
648
743
|
const statuses = new Set(result.expectations.map((expectation) => expectation.status));
|
|
649
744
|
if (statuses.has("failed")) return "failed";
|
|
650
745
|
if (statuses.has("error")) return "error";
|
|
651
|
-
return
|
|
746
|
+
return result.status;
|
|
652
747
|
}
|
|
653
748
|
function testRefusal(error) {
|
|
654
|
-
if (!(error instanceof HttpError)) return null;
|
|
655
|
-
const code = TransformTestRefusalCode.safeParse(error.errorCode);
|
|
656
|
-
if (!code.success) return null;
|
|
749
|
+
if (!(error instanceof HttpError) || error.errorCode === null || !isTransformTestCode(error.errorCode)) return null;
|
|
657
750
|
return {
|
|
658
|
-
error_code:
|
|
659
|
-
message: error.serverMessage ?? error.message
|
|
751
|
+
error_code: error.errorCode,
|
|
752
|
+
message: error.serverMessage ?? error.message,
|
|
753
|
+
tables: error.tables
|
|
660
754
|
};
|
|
661
755
|
}
|
|
662
756
|
const CHANGE_FAILURE_CODES = /* @__PURE__ */ new Set(["transform-test.transform-failed", "transform-test.unparseable-source"]);
|
|
663
757
|
function refusalVerdict(refusal) {
|
|
664
758
|
return CHANGE_FAILURE_CODES.has(refusal.error_code) ? "failed" : "refused";
|
|
665
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
|
+
}
|
|
666
862
|
//#endregion
|
|
667
863
|
//#region src/commands/ci/unit-stage.ts
|
|
668
864
|
const TEST_TIMEOUT_MS = 12e4;
|
|
@@ -680,12 +876,17 @@ function transformLookup(entityId, entry) {
|
|
|
680
876
|
function clientPort(client) {
|
|
681
877
|
return {
|
|
682
878
|
lookUpTransforms: async (entityIds) => {
|
|
683
|
-
const result = await client.eidTranslation.translate({ entity_ids: { transform: [...entityIds] } });
|
|
879
|
+
const result = await client.eidTranslation.translate({ entity_ids: { transform: [...entityIds] } }, { signal: interruptSignal });
|
|
684
880
|
const lookups = /* @__PURE__ */ new Map();
|
|
685
881
|
for (const entityId of entityIds) lookups.set(entityId, transformLookup(entityId, result.entity_ids[entityId]));
|
|
686
882
|
return lookups;
|
|
687
883
|
},
|
|
688
|
-
|
|
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
|
+
},
|
|
689
890
|
runTest: (testId) => client.transformTest.run(testId, {
|
|
690
891
|
timeoutMs: TEST_TIMEOUT_MS,
|
|
691
892
|
signal: interruptSignal
|
|
@@ -694,21 +895,80 @@ function clientPort(client) {
|
|
|
694
895
|
};
|
|
695
896
|
}
|
|
696
897
|
const MISSING_TRANSFORM_DETAIL = {
|
|
697
|
-
"not-found":
|
|
698
|
-
|
|
699
|
-
message: "the transform is in the branch but not on the Metabase instance; load the branch into it first"
|
|
700
|
-
},
|
|
701
|
-
"invalid-entity-id": {
|
|
702
|
-
error_code: "ci.invalid-entity-id",
|
|
703
|
-
message: "the transform's entity_id in the branch is not a valid entity id"
|
|
704
|
-
}
|
|
898
|
+
"not-found": TRANSFORM_NOT_ON_INSTANCE,
|
|
899
|
+
"invalid-entity-id": INVALID_ENTITY_ID
|
|
705
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
|
+
}
|
|
706
959
|
function unitStage(checked, connect) {
|
|
707
960
|
return {
|
|
708
961
|
name: "unit",
|
|
709
962
|
run: async (report) => {
|
|
710
963
|
const port = await connect();
|
|
711
|
-
|
|
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
|
+
}
|
|
712
972
|
for (const transform of checked) {
|
|
713
973
|
const lookup = lookups.get(transform.entity_id);
|
|
714
974
|
if (lookup === void 0) throw new InternalError(`no lookup for transform ${transform.entity_id}`);
|
|
@@ -726,36 +986,10 @@ function unitStage(checked, connect) {
|
|
|
726
986
|
});
|
|
727
987
|
continue;
|
|
728
988
|
}
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
const check = {
|
|
732
|
-
kind: "transform-test",
|
|
733
|
-
id: String(test.id),
|
|
734
|
-
label: `${transform.name}: ${test.name}`
|
|
735
|
-
};
|
|
736
|
-
const started = port.nowMs();
|
|
737
|
-
try {
|
|
738
|
-
const result = await port.runTest(test.id);
|
|
739
|
-
report({
|
|
740
|
-
check,
|
|
741
|
-
verdict: testVerdict(result),
|
|
742
|
-
warnings: [],
|
|
743
|
-
ms: port.nowMs() - started,
|
|
744
|
-
detail: result
|
|
745
|
-
});
|
|
746
|
-
} catch (error) {
|
|
747
|
-
const refusal = testRefusal(error);
|
|
748
|
-
if (refusal === null) throw error;
|
|
749
|
-
report({
|
|
750
|
-
check,
|
|
751
|
-
verdict: refusalVerdict(refusal),
|
|
752
|
-
warnings: [],
|
|
753
|
-
ms: port.nowMs() - started,
|
|
754
|
-
detail: refusal
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
989
|
+
const stop = await checkTransform(transform, lookup.id, port, report);
|
|
990
|
+
if (stop !== null) return stop;
|
|
758
991
|
}
|
|
992
|
+
return null;
|
|
759
993
|
}
|
|
760
994
|
};
|
|
761
995
|
}
|
|
@@ -766,9 +1000,10 @@ var run_default = defineMetabaseCommand({
|
|
|
766
1000
|
name: "run",
|
|
767
1001
|
description: "Check the transforms a branch changed, writing one JSON event per line"
|
|
768
1002
|
},
|
|
769
|
-
details: "Compares --head with its merge base with --base
|
|
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.",
|
|
770
1004
|
requires: [
|
|
771
1005
|
"eidTranslation.translate",
|
|
1006
|
+
"transform.get",
|
|
772
1007
|
"transformTest.list",
|
|
773
1008
|
"transformTest.run"
|
|
774
1009
|
],
|
|
@@ -787,12 +1022,12 @@ var run_default = defineMetabaseCommand({
|
|
|
787
1022
|
},
|
|
788
1023
|
repo: {
|
|
789
1024
|
type: "string",
|
|
790
|
-
description: "Path
|
|
1025
|
+
description: "Path inside the git work tree to check",
|
|
791
1026
|
default: "."
|
|
792
1027
|
},
|
|
793
1028
|
results: {
|
|
794
1029
|
type: "string",
|
|
795
|
-
description: "Also write every event to this JSON
|
|
1030
|
+
description: "Also write every event to this file, as one JSON array"
|
|
796
1031
|
}
|
|
797
1032
|
},
|
|
798
1033
|
outputSchema: CiEvent,
|
|
@@ -809,8 +1044,9 @@ var run_default = defineMetabaseCommand({
|
|
|
809
1044
|
},
|
|
810
1045
|
job: randomUUID(),
|
|
811
1046
|
now: () => /* @__PURE__ */ new Date(),
|
|
812
|
-
write:
|
|
813
|
-
resultsPath: args.results ?? null
|
|
1047
|
+
write: jsonLineWriter(process.stdout),
|
|
1048
|
+
resultsPath: args.results ?? null,
|
|
1049
|
+
signal: interruptSignal
|
|
814
1050
|
});
|
|
815
1051
|
if (state !== "passed") throw new FailedResultError(`ci job ${state}`);
|
|
816
1052
|
}
|