From 39c0c54ec6b8915b78a0e4b417c918113df9db5e Mon Sep 17 00:00:00 2001 From: DevForge Engineer Date: Mon, 18 May 2026 02:03:08 -0400 Subject: [PATCH] feat(cli): add --exit-on-destroy and --threshold flags for CI/CD gating Adds two CLI flags documented in the README CI/CD section but missing from the actual CLI implementation: - preview --exit-on-destroy: Exit with code 1 when the plan contains destructive changes (deletes, replaces). Enables CI gating on destuctive operations. - cost --threshold : Exit with code 1 when the total monthly cost delta exceeds the specified value. Enables cost-aware CI/CD pipelines. Both flags align with the existing README documentation and maintain full backward compatibility (default behavior unchanged). Tests: 53/53 passed (49 existing + 4 new for the new flags) --- src/deploydiff.egg-info/PKG-INFO | 17 ++--- .../__pycache__/__init__.cpython-312.pyc | Bin 291 -> 283 bytes .../__pycache__/cli.cpython-312.pyc | Bin 7577 -> 8990 bytes .../cloudformation_parser.cpython-312.pyc | Bin 4087 -> 4047 bytes .../cost_estimator.cpython-312.pyc | Bin 6485 -> 6412 bytes .../__pycache__/diff_renderer.cpython-312.pyc | Bin 8113 -> 8066 bytes .../__pycache__/models.cpython-312.pyc | Bin 6502 -> 6494 bytes .../__pycache__/pulumi_parser.cpython-312.pyc | Bin 5296 -> 5281 bytes .../__pycache__/rollback.cpython-312.pyc | Bin 6617 -> 6558 bytes .../terraform_parser.cpython-312.pyc | Bin 4517 -> 4438 bytes src/deploydiff/cli.py | 30 +++++++- tests/__pycache__/__init__.cpython-312.pyc | Bin 165 -> 157 bytes ...st_deploydiff.cpython-312-pytest-9.0.3.pyc | Bin 74277 -> 80318 bytes tests/test_deploydiff.py | 65 ++++++++++++++++++ 14 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/deploydiff.egg-info/PKG-INFO b/src/deploydiff.egg-info/PKG-INFO index b92bde0..034820d 100644 --- a/src/deploydiff.egg-info/PKG-INFO +++ b/src/deploydiff.egg-info/PKG-INFO @@ -33,6 +33,7 @@ Dynamic: license-file # DeployDiff CLI [![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/deploydiff?style=social)](https://github.com/Coding-Dev-Tools/deploydiff/stargazers) +[![Awesome DevOps](https://img.shields.io/badge/Awesome_DevOps-Submitted-grey?logo=github)](https://github.com/wmariuss/awesome-devops) Preview infrastructure changes with human-readable diffs, cost impact estimation, and rollback commands — before you hit deploy. @@ -69,6 +70,12 @@ scoop bucket add Coding-Dev-Tools https://github.com/Coding-Dev-Tools/scoop-buck scoop install deploydiff ``` +**npm (Node.js wrapper):** +```bash +npm install -g deploydiff +``` +Then run: `deploydiff --help` + ## Usage ```bash @@ -154,13 +161,3 @@ DeployDiff is one of eight tools in the Revenue Holdings suite. One license cove ## License MIT - - - -## Install via npm - -```bash -npm install -g deploydiff -``` - -Then run: `deploydiff --help` diff --git a/src/deploydiff/__pycache__/__init__.cpython-312.pyc b/src/deploydiff/__pycache__/__init__.cpython-312.pyc index 36a7a096fd7e5b13e1d6ade122683776fe528735..4f7fcd75b6308797f7182494f3f80ac116cbfe03 100644 GIT binary patch delta 53 zcmZ3?G@FV0G%qg~0}w=aa!ur3Am{3A6%$&VT2vg9m7klL8sn0mT$-DjS5h3~o>`JH H@nR4FnA;K0 delta 61 zcmbQuw3vzeG%qg~0}%KeVx7pnK+V_LDkiizwWv5IBR@Aa#y>CBr6{v3HO3`BximL5 PucSD}J+s7P;>jQYHlh^i diff --git a/src/deploydiff/__pycache__/cli.cpython-312.pyc b/src/deploydiff/__pycache__/cli.cpython-312.pyc index 6be352b7287d7cd998c79c350cf318fa46817354..801b0a55cae882863076bef0396834414bbe1866 100644 GIT binary patch delta 3226 zcmd5;O>7&-6`tYl@=u~DN)$$nrjk(ub!(ItlhZG!+UU<8ok8WM=4O%J^l1rw`9VW2;6 zmZU6M^0^&i-@KXm=FQA|-@M_^N8UNsaM$B;BPjb9+|!MbHyT3tH*fS^>vSO%tDam- zK9CAvWJ5$+;3z1m5VVBp0jAjod%TN{3%Js+`mUoIrdFpSBr-h=$PWB0^ZA=yO=`0> z`#Iif^Dnln&kigG7sQGs3sJ#`)Yhlq!`tE`YV;}iw(ao2cC|zGtMX!O>)O6f80mr$ zp?bbsZ#y?M$BC>8sgK|y)Wl={rGUSif9ftZd0k9(sk_yFsJmx{E<}Reh&nJ*8?K{N;3ag+ z_LHAoL05&B&=sr25ZOqP;~TsA{AgJi0Nw`jq2AP9a~56YWxX|AK! zaS@+JifwEQu$cqx7O;o_g2z4pQ4%L-w2Z+m$&U_=K0E$BgHU69TF+;Rcl?N+naLB) zG{%o-%_+tl-E%ILWOSM~bz10rVsFXka(X76GY%%#F@CUXI6%y!)#l)h6AwW}vyeprW{VaH32(Dh6ltv`qqYBp|8E6Wrs6F1JK3h>;rNblsA#N?h+N( zZZt--Hbr17_7_J&02-qWAgy(++15fOF`v$AGy)_1Gq@Ay(V7!U{#)ODAjW85>5mQ( z&vih5!Iwyot64LlYYCMYCe>#PCyzphd<7gMXLOawyXEYpY)%pRQZB7Qmu7-_L$=^E z88dsC$eF3Mc7YgjTqQYTf}&HI62NDOp=2ajs8)EOtk5=L%>M0c6QrWV+FdVViRE0O zXi=m`iqf=|JZn=bH63EItD|2sx#O&|J+9`d+eo#)EwDo_**=SBvEpDiT;D%m@Vn1a zqP~1;(@_e?wmD7QP?9otMOM48$s6%4E>n_c`?*zE#OY4Vm6|vBW}@nwl8_^sVy98q zJC(VJanS>!@@j30o2(1uWb;Fh-B zJN(JW{kr4qhP&^?1F`?lj=)=cRw9F6L2aemnuDc7Osp7}ZUv3vhqp)QdyrzkO|^qg8ay)8v_ zGo6$3z>lfiEbzoI!K*nlEt9JmLR14dlr`kb>D&yF<0N?@Dev92OP&O(bc3QO0&R!m zlpHFVOwZ&@%K2I2P9x|D@e5~>C%%b0r=_B>C@%232IY0TSy#~zD%y*ZS?x!t_UK?# z6t9f}efZ6Iq7=o8M<6VNoNN+sbGn{Cs$TnTQFRnWmDkR3 zI2Q^R++`)t1SOhE=mZanQEE2%wkE2P0@6W{6vvay?{M>U2yh4BGkyUA$Qfp@xu0RI-hb{7;?A!fjmwVc9sf#f_+D)IlNY{-9bM<< z-jCFkePj3bjs4zO-g_D-c?S2a_9Wjo?wo|io}sc}^Xy*}L?Eeq14%uo^B4xBOTVnI z!$XfFA#Ljq9`dcXFv0%nNk}6wc6O<~LB)r8a^;VRmd=wgMJUy0;GMcSVd@k4%q4md zMy>oxt%F*ROvMEPAATN&FD`xN{T<#-cXH?xG2t1E^2<#*Q8({~{mQq)Z3U8x%dG7C zQW90MC@kG;`q1VPUeT|@zN9nhS8{~?v?Yd*vkzNzi4Fjobp<{Ri8Py;qQ{|Ga&t9l zawz={`&Z!An8k}dd{H;u2bC4n%J%pSK)oA05FX?z7dPQE#^HGL=r3aED^XthNAM(0 z1`a=Pj#}@H;eP1tI+rZHDj^1WDPb{dcfl)>7RI`Aa%Ak5Z~r z+Q)8&BRIkq!jUeXElbj5PES|JX~m~P4(p`wYO_Cu1MV5X!C8^f;wg)E-1?O` z^i2b*YhpViU49$xd?+B>ZZ5oK5t?N;`*rjp`#jRpyNW&YEkBH2i>`{n)%wO&S8B~6 z+HBvr8*ubt9cX-3n%#-E+WpW~?ySyqv-xPCd&6wss;j{=aBj^dfr0u)FyH}=-Hx`~ z1JEk>!rc42j^KZmUlWkC`CBC7*1y{XskJPC_Q>G>Lw<|wCw%Cx&v~LvxEsYF{|m?h B@FD;J delta 1832 zcmc&#UuauZ7(eIUbMv=Jn>J0eByH|(*Vfy1t&MC>yNPVA8@P27Y=sypS?<-QOLDt& zQ?(;?9TZ<(J;E54G2a}C`VbU+@WmG&d=i~(K?e#x2%;TTgdyVhC8@J>eH2f)zx$o@ z{mJk9bGTQZx-#Ma+~@NUSgS40h4(`1{xD6f4_*vP#8k?74lCISJ9%M5RHQf^6<>Hd z`h69DnKraW>l)d%+IAa}nj>E!J`A#8*ta$y{5c8h0}qi-0!M92O7>QQ(mYpJ)>t8(H>Y*E){=p#Iy+ai=JJ_Eo&arqBKSB+jge}Qlu^AO z4xg@m6#R^i*W{d8xW6VobjS2KmU|}~@qTE?Q!gaKtoT0kjZ-#YY8G7L8E>$9tLuv7 zQ=Uzq$(g`UK4ZNqE<`kXSiBRll=Jdv;ziLJ{WR?1+06WC5zY7^2vlQuzz>UGqURWP zN4(J%>hka*5bKq1a70rux)?hg(NRRO;IUJ1ZC1$-O#POHt2bk(=xF%J4c8Qg)`W_q;8@XyY-=dyP3`J>Rwsb;HRXb`}!MuP=I={bNJ z!(h55KIpkd$HeJ)T;jcABOaZ>>sWJUtb9IQFlr8Kv6!_AHFi2(oacBw>T^1jGroFUY8SOD57dLhB}j?WSKud(VoW y)lSbH$w?nZOnBBq}-8QbVU5F^*B5rY=%JjrE`kb8Tw}b!oiu$Gw;05^UOQ*9u@y67QPFD z9pKUb!aiKMD){|*3CPSa^Ufpf?I$x*G%?@_p%cgkhDboerCN|sEPmHZrII!sp+zWtODy=NHF zbfJ{3p}{c$bpiqfnqn?ed>;x{*pp|0DYr=umz>6D(R8CWx{@qB?T%ce2)UZM9sg*7 zu7Q5^m9CK*Au#xy-vlcV*_en8DtMw6Cd7}8g9)+^jKff1U1yr$`Wo8`=^E_Nyy%!) z@Ov_jfuUGpP}NoA!LXi8WGE$O#4_+97JRg~PKU8J!)xl}k=TGbswNDr+Hzm8AY^fj zq8hRIh^}33t4bXC)%!^zRO8f{Mw zjNx*Op+`I7#t@q4d+j%{*>BzQY<-{SVQ`l%+6vUA1Cdl9vQ^*xt!q2bdq5He^sJDj zfx8j~gc6Ypq}gDK4K8_?&+o7`=%%nXeG7tu^6Xs*Y~1tCnae#$o-Q@-w?sMHia-%N4+iBZmI2>;} zZQTXjrp#DzF~k_6v@7V7qYc)h14jflp^#YXx0>)C9AE_6bqpD{X*8*fji}99A2P(J MTqlIIb`LXu0re5z1ONa4 delta 900 zcmZvY&rcIU6vti4z|GrL--?z#t<${ttVuA7+?8?NHG`(W!sdst(+2C`b9HvvL*))?CfpIqk%YhDvx2 ze=tE*=Gu()p&L?xaal%*<- zT*5xucgw4Bi>$;G5Bk#alqkDMAn@OH+}(U7)+LLQ9J@c15@Q`{u|-ObiLsWU#HdzQ zjDG3dok{HZwF01DWcUOr(1+(wmL+X8PpJ=)>K}%vS)sjno|T*7K13YpYmwub^^^DMlqyqH*-SZCjPcgt_= za_x_z_!m<+(+`2ovBwr?+2SvNGM5~1&iV9m;u9BIjUTx47bh1cmpQD?>y8kxhxfQp zmJ9tnL_~Qk2T7|>o!5D=#rva0XzQ{o%IUXFX5w#i`l!#m?J{b901_M2NsUB5<&YIA zpaz6wLg|qeykxD0wRp!`0~;`8s|vIcyaePQ$*enw**DdvhJ?{U@tPFF?d+5}0--_b GA>=p9OW#@m diff --git a/src/deploydiff/__pycache__/cost_estimator.cpython-312.pyc b/src/deploydiff/__pycache__/cost_estimator.cpython-312.pyc index 4fa56de16c3e9566f857d61121397284da0d3e71..badba66bab6f14eec828a03349f7b2db11d98dfe 100644 GIT binary patch delta 1079 zcmZ9KOK1~O6o%&}GnpjQn8rM8T5W3W!_ivpOKYF@q199>F5IMnA?=yAsY#=ECWxpM zL41IShty-KwXSgwaoVe5i!=z;B!fE^_nO}&3iJCXW?ryr@(&;Zh)h>2%q^`C~b z1S54qF$)=Aii+){V(X~QbK-$L;{y9dt)yX7FjNZOBq?fIuej!*xmz}Dyh0M~tIWlW!3rl6& z^?*b*+;IY#W0qlc88gM%!=_EO@?Lp8AsSm$Y@(IlDf2Q>*@XXk{p46*`jBN(E4^6A zXU+6@VYZkzmuxFNk+m0CKpR$gyQT6$ZOZLQAX1IpTs1>H%o`jct!yILRK0_Td$_c6 zNwO2cj$l8>_TO@k-4DJXgX~iElXhVj&i%OUC}8I7j3pyC;6+8^HlE1kH9H;JkBQGi zadL@$4qbEIChXkmZ^Xq;g-2!GN1NHxa3ASrB(jbqSt3#s5f$2a1uh@(w(hXW$VlvB z?d79yLeVSnmH5>Tw)VW|yIb-{*p0}cmdN?qA66AwABAG0{qD#8%9yO`WJ>qYE?&vY zrJE(9z00D1A76_zH*w;}apajoAxBM)%3Q9Hv2`!~YZfP-=VpFvDu{dO0Tf-P-JF4o zi1M;}$WaXg3bTgc_zh#OXcwv3YZ%ncZPO@@dL&~niH-&evq`c TQq3nR`BF-j&&Hm)WS;#C?XLy2 delta 1131 zcmZ9LPizxM6vk)lU2ptn<2ZIwI|M>PNLWfK2~B|1N$NNhD3%eTNYTow<>YN+Q}1px zyJ?$KE*v-@Aeu@bVI+F!q3WRr-{_$#QcoOORpPRMs03Ao0Er8#)KlM##UIs{KmXp_ zZ{L11yL)l+?xcQ4*JA`frQ7Pchg*7%{=PN7&!C_-BerI0lt2WuW|ti`qjt=U*>N*& zC(MMcn>r;8vgHhHLSHMvDfGiTkcAB7U;wtkAZ%}G*xdm`u(LsthLJ`j?Lx{*8byjr z+Kn_OX&h;fBm-%$q<3K-ya)T?eVl)wrQ);+Jl2`EIf-~s+CG3On1(~J6Arf&G#z=NpFOW5`RX^ zdkq?d3(m$9ilYoQNEGWeXVogK_zl;2Rh@Cy*^1RZ75bU>>F5je+E-rHJ6K&_U-`oF zncm)|3{$G}qpDB!_APZ)p;0~(yE%5II8!<6S)L&%e5s5z_*$1TmZ?bfk NEVXC*?}QXF;XkuB7@+_F diff --git a/src/deploydiff/__pycache__/diff_renderer.cpython-312.pyc b/src/deploydiff/__pycache__/diff_renderer.cpython-312.pyc index 144363cca8f7418e2a98ed4bdebd0f985dd554b9..9548f969a22e085e5ad656fb2b098829ce572357 100644 GIT binary patch delta 1247 zcmYjQO>7fK6yDAL*s*t=+D_v5H|r#IaDuE}5}=71mEVK{iWGWrBe@&T5Ib6Xt=Wwz z5fZ3ELP#8%j?`0;5I1@#0^d<3jy-XxDy=|Vijdk%df?7`v%z8Ief#EnZ@&5Fz2~PZ zKdzX6nr2Lb=ev8c9b@0j%B}s`)l7s$op{Hn8y&N5b`tf3EKy^p#Q)dQ+Aw$)dPk*SR<_(9s&WuH<90pQq2Oxkc9%NnT(!3x+D|IKsN$||@) z+-tel8!c)R$^!lCrr&nEfx6{7gpKo`jbHQ#WHFW3pgU#0YECZB;g$2{b4912Z#HMYr(LT;MOv|K$1plf<54MggrV3^lPDUrbe! zU!Xef(i zR>3@!FGR8#OxBvN@DiBCBBrN3uR7X9-2nqPv4e7=ljur6kQy6iNBdRZLUGE zs9AynKe&hHnm-4fF= z(rDP-uIt-4%U+`qP6`fwID4YAqDxrN#}dP8iZTb$Wgy;l=vV9}U^2Yk_h1hcS(XE7 o;jJ|FRvLXRS+Au6{N72+FQw&!sH7$TRx>ZvOy4h^l2xSp06}3V7XSbN delta 1560 zcmZvb&u<$=6vubHySC$a?ZgQ<{*id=CXSs(;fItKibbR}r3r0QTG|R(7Tb+y(l}c0 zTC?lYRtN*Cgt#D813e&42nlhjkpBT7N2E$URjkw#R2;bUA3);0Ss<;nEBnpP=e?PE z-#2eRUHfrO|5evh5IK=qySO=&Nx~lQrCVuPB1tl4>%B}X)62H9y@}R@ zEYZw<6&F4KkQDIw=NZn(IPh8oPXn(6`{0rSk>gQ8Hgd{ZTsez@v1|0FTmH03M~Y2s+E4 zvqVZLlT^u3GJ8jB&60UCcSmi>bNpJYP+A}rm{drO7%&=SksN{X$gOgt&cBRp^50{{ zY(oyz%k-uXqq5`P9Q_^pMwM0mb)qI~{O82|+NDz`+gCiwy!M{kqwNb0-C*4tw7ubW z20iNdUi(bf-{fB;-_VXhF7?ra&d*k#*kg^Cd zJFY$GIjqW;^~!OqZ;(LD+z$f9^BF4xY9!cET#nZjXuh>$Q+5KuYiM5QpXm84UICl^ z&usDU^}poh5zpLK^f90fMI6pv;>BET@)eBBg8|vM`jqAP#oWyD6awWs_1QG~@nnXb z!wz$gzq@t*opYzzasE|qNj}Mc%AKf-)D|%g(-%py1$YZI(B`s0v#lL!vnH=hErn?s z7>C9hy*Kr-40--G{f8142cG8#Gwb@>;B3U%Wel9>xx#9=@FLF9d_KBbSc8T9Uh!Z{ zR4wIEpU_=vVEYV{3F212PaOinF~J~yow(H$oN^t{wW;x;vF$pPE%0o)xGLoTAH-A)@1ofU(?~zFiKtNuU3LC` zxp)>g4dby~Y!A)h%74|Tksd}K`qgK4?(&23%CH(FOp~}B(+pCkxjXO&jG87xu|b-3 zJ9{ggP=GizND0)p9Jm|}V?q$N7hZWVCL(Ila-&FnHuAgpIn-onpaXBajwJ~8teGp~QRDy)ir5_+g<;Eh;h}!C|cSdDn z>^(53&Vy707D0#v#=i7aFA1?m7zDx6Lr?W5^xC~^1ohB``{8i!_uTI}=kD82ZT(Q! zHG!>}JK9p=mHtqK*Z4xDAYzJGKyaKSLBcB}OXMJg`=nJ`6!t{C9ZE@#?1b?>v5CTN z&X5+OIvPf$yAZ-hk_8cbBTZV-b?{&-5;O${id%*x%#lrFvuOn!*o*}_wF4cGq-^_t zbQs7NL9c$0J0Z~&Bp(Zo@k9#LKnpY$#~y@-iMbcTk8%&rhle4KJK;Wa1Oz|A8{;bw z_5i-EzUNl-++GsjH=;Q}Fy*->s`F?#AyBZq{L)cJcgE9NqXbaw$D^ zb|in* z^K*0@esyGT~@c{ODJ~)K8iBgVR<>98UO|X2H3~#WB zmCYmUw^RrSw~z$)g(M2V3*lH&n@0{>P`#iR0)8PRa9LalRf4m~;wxcKBc`Xpdto5< zpE&Tu2^6fo72A;!@|quTR`{XdDW2c+Q>`ybLpZYPgLmR?xGW7J1D2${#sS2+D!UmQ z9%mtH`iqnjd5cbHkaV3=H(H~bOZmTw%YL?uKa2*j?R8FE+_^R0L30jY| zGwftYGITI>QJ9!?z!m?uzCCOw%Fx2FouQSYt~jn;yx^9L_SPa$Q4{Je7Szem1CnKO<}oXpk2jGdX7wZ}+q-p-5_vU4-`tm|Zsp+=Sj^9l==A5 z=tsSK`S9oF*xmA-@{$hTZQ&>8BIYN(`-9#Mzk=}rU4Gp!rgeEO5=b}8YjKfsor|Y~ zcs(8+9}(A63ezKET2eN!MD>P-(!ttfsEAMuUWS`?G;SmoFA~>rc-lUZAmhycz6NUF z!^5a2Mzx9RV!lwaiOFxi+7#XVCu5Lh48nBegnXJM2;?ACBW6GAKzbR@QQQc*bH%(p z?YN{TL2;hOHGbi51nC9UNT5FGF;XZ6myCQTdz(#1tjP*TSc8Vox;#8nu;%ji2+6?@ JV?|&mzX7~ax`zM& diff --git a/src/deploydiff/__pycache__/rollback.cpython-312.pyc b/src/deploydiff/__pycache__/rollback.cpython-312.pyc index c0ac66222b624a7179fdb5b7fdaee2c5f7668cee..b1bc0b5cfeb6960d8c1bbc034f763007f7c764e9 100644 GIT binary patch delta 476 zcmYk2J4gdT5QcAZdoj5f$-}4@G)Tl3#44SSAZQRltbAck!jU^P(Il|DK@@Dn#!BM$ z_SV;Gsf~pQ7UmkU5U~?WD`z8$3;X>$GrRK-+sa;K!*}7ZuHY)1>XqYW*ns)w=ujFI zTer0nvgt?lqjsQnpaxI_sKF**2Vc`mx&#J%poz44l*-RdnybvA%&gR_WyhSaml{>4 z#+kWL<~!m>%|px$ksA#n;vtv{MbVc^cdENz!C8Q~I~7_}A>p2PeZoo)R=fbU_KUW~ zEl=HcIPI0U`y>p+IH3`F9zb|H<5p?6ShK1QO<{r_x(_UV)tJH=DvS?ex55>s47 z$F%OXN3t&7ql*w1XT)%qVh7UxHgN@$(*B=cQijr$_9K{z`0L#z(-IX~+7K_1xO?Bb z2qSTfmw%`9W8nFz!yB}A7~Cr}USOPVjq4(3Oypx&{rQacq_W$kLS=*Y{9a(XTDKc} ePL5_U2e{cV;zI?1mNN6Iq~DZ`xHkHTeDe*)GHn0= delta 588 zcmY+AO=#3W6vy+LkIkl=bz8HotG2Em)NVmRdQw!BuGp4prD`qPlrkhkyP6M~$to4R z=~Y;A(31ymdXpUWBzV%By>y`UBwoe!=%H_-1r5yaKkuK(`@hV~$*oEIlWkiHK64-R zyRW)-4d%Khk57a~%Logt0w})a>rb?niE5x0Q46RRs)^d)4ep%PL}_ZBL~Hc2m$c(( zueup`m`8=xr1yh(<3>OtQC^@tcmJG!yED=UM*6$&@z}MgbM9?U8F$y?kh=2`ZL;NaDq6VfQ*?$vfOojzBufRk;jAN7eroLZP(9EK$FxiGwZ1g@f8uh-drR94E7eJ)-C zsAhKmq6)*AU;YWV4&+}m@^IBB2@&cVO;}ByJ}7Yr(a@NTU`G(b;tBEYtwtoItd0iP z5E%0}r;HUVr*A4>^`YfWZ6$p+Fs=_RZfZ;EyMY;~rbMe{qk|hV{zdgYoRRVW9BIZO zEqbP9fe;dlxEDyO^63Vt$n3gdQL!dM2&Z-%DT>(84Ld_Dfhv%g7k}b1(wsT>ynF6D z^Jcz@KJMxNNmXU!Tyr1GOT8LGztN&`i8X@v|3UfwJud0`!Uf&`H_&j)+3H&$A zFd3FfvhV|QT;H z3L+M8jprmH-Nc`a)XbWglp5RRK{dexP(cXMP zl}JP;3S1LHagFH2AV-MXVu(9~lbCqE^A?o)0eA_w6l(YmpTrB}LZ9k842cnBgcH~g zUkQW8I7Zp}1cV4GVE9!S=AGF|3~eq`d1DP-V=tmLYzE-0>mfwM`?w1pi9>qW*~5xZ z=%EmWpfuRt=g?hO;>u#?a?0Knt;MxV$;IrV<%I?57WP3!^838B{W%H&+Ra!AD+t~4 z#0yKJ`+tgO9SWs$0{3mbA?E_M#yvr78Vjvn9E<0XBp>>ht&_`qLrA*sFUX|a!#I^qv} zWo_aa`@p@oOZtmD`pC9EQqo7u?y;u5t;)GkrwPBfPX^~O(n6KDF#2CeQ;ps@#US0c zqeZv1=%0v{jy*B_>*-HJcTesGJ2$T9uirI*taE>{=tyKEln?z|;hm{;2`< z<3RWH7`r70rcHh;EKzRq(?inM^DfFqv4e@`Z|k^t=VlSY`+E-AxL;KBSbWK$CTq1p zTI<$ca|&y?0q?-)+WYur@av}0=7{e}SJQTO&dyp3)_FLm591T?q5isXmKtW@u^x%h zFLN;9TV%~rPABe)uPm=5SFffftOci}iEJAp#$5OhO^h)&)uPtBk3`Ju2QFYNRF@bH In+Nv)0PXb>V*mgE delta 1275 zcmZuwO>7%Q6rNdo*Soei>y3Z%M{QC!t?i}+8d5|EX+teLCZugZ35Ggt9j|E|H4d6} zk|r8MBrY5{#2ukFYN}RLl|TUrR3Y^WCn5y3v4xSzjUG6mf-6-7Au&@s!J@pC-n{wd zecyZYvp?>=<@J3l%Vt3Bm9P03UIE}2HrX+$7QkvV%=xx*D=wpES{I23*Puu~f@3_g z&uO!qNt;Yn!oo1%7a(t|4jW-Kv&4W@*5zbLW|gt!D$ts+ z^(weVo-lfw8gi6cKZ>oPM_Uae-y1DWSv6B{2!J~LEbpU7L^nYYAF}*s71~M8c%ZgU z7nWgXZ!jN>u-(nD0CSHO7&AM<=pbFv8S%;gwe9ia{jqU9iS*d? z+-x#-GMx;f)Iu^AoJ-8lCes-`7EWb`$u)D>9HuI)x;{mIGY4(1_RG;+H1B@ubFS#@ zT{%IHS-yLBYxMPe^h`NARftaI6Z6IBLg^wdjb_(-uPj_&UR%xw2JTE1Jws(rsNe|| zJ;SAr$O8a^T$rn}A;RUw)~%88{6wrgajr0NE+0q53B5GGR2o@c_g;DH`pnu)-rs-c zrK0<2*&QsngGG0!)E=fHLtId1Dnnd|dtbTN?jx(#p|%kS6wm!r5IolJuU7B04uQ>4 zR=fqp`v)*c`|hdE)zl4pN$vVr-D>H$yp&tIp%T1md}sj8`^#!qLGAkcK2I~Zbw53e zOU~hL@J+WT(r?(bxFP|5vr}SmfRFS_n>{8LKMy1Qx6|@xu&<9Y{077xZN>O@JcTOe zcp{UUOP|#&0k0kqBC^Lel8u8pKYi8KUrJL);$I5p5Ij| bz^%x6Wq%b&kgK-7459GgO-_OSM>_ui9cwj{ diff --git a/src/deploydiff/cli.py b/src/deploydiff/cli.py index 46d8771..143259d 100644 --- a/src/deploydiff/cli.py +++ b/src/deploydiff/cli.py @@ -36,7 +36,12 @@ def main(): @click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file") @click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file") @click.option("-v", "--verbose", is_flag=True, help="Show before/after details for each change") -def preview(terraform_file, cloudformation_file, pulumi_file, verbose): +@click.option( + "--exit-on-destroy", + is_flag=True, + help="Exit with code 1 if the plan contains destructive changes (deletes or replaces)", +) +def preview(terraform_file, cloudformation_file, pulumi_file, verbose, exit_on_destroy): """Preview infrastructure changes from a plan file.""" plan = _load_plan(terraform_file, cloudformation_file, pulumi_file) if plan is None: @@ -45,13 +50,26 @@ def preview(terraform_file, cloudformation_file, pulumi_file, verbose): render_plan(plan, console, verbose=verbose) + if exit_on_destroy and plan.destructive_changes: + console.print( + f"\n[red]Plan contains {len(plan.destructive_changes)} destructive change(s). " + f"Exiting with code 1 (--exit-on-destroy).[/red]" + ) + raise SystemExit(1) + @main.command() @click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file") @click.option("--cfn", "cloudformation_file", type=click.Path(exists=True), help="CloudFormation change set JSON file") @click.option("--pulumi", "pulumi_file", type=click.Path(exists=True), help="Pulumi preview JSON file") @click.option("--pricing", "pricing_file", type=click.Path(exists=True), help="Custom pricing JSON file") -def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file): +@click.option( + "--threshold", + type=float, + default=None, + help="Exit with code 1 if total monthly cost delta exceeds this value (e.g. 500 for $500)", +) +def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file, threshold): """Estimate monthly cost impact of infrastructure changes.""" plan = _load_plan(terraform_file, cloudformation_file, pulumi_file) if plan is None: @@ -61,6 +79,14 @@ def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file): estimates = estimate_costs(plan, pricing_file=pricing_file) _render_costs(estimates, plan, console) + if threshold is not None and plan.total_monthly_delta > threshold: + console.print( + f"\n[red]Total monthly cost increase of ${plan.total_monthly_delta:.2f} " + f"exceeds threshold of ${threshold:.2f}. " + f"Exiting with code 1 (--threshold).[/red]" + ) + raise SystemExit(1) + @main.command() @click.option("--tf", "terraform_file", type=click.Path(exists=True), help="Terraform plan JSON file") diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc index fbeb799cee2719da9d6911ae8c8387fc138bca19..aa9d6d767ca61f3b45648939ded03270e7587fab 100644 GIT binary patch delta 53 zcmZ3=IG2(8G%qg~0}w=aa!uqmma}oTiU}=FEh>)5%FoSAjd96OF3nBND=Cg~&n(H9 HnBoWkh)NLM delta 61 zcmbQsxRjCmG%qg~0}%KeVx7ostmfiu6%$&VT2vg9k)N9y}5CF~$)9B5@P< diff --git a/tests/__pycache__/test_deploydiff.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_deploydiff.cpython-312-pytest-9.0.3.pyc index 2d4339e5f2f7c40714ddc60bf935c2e76e7386d9..fb025d0ebc30135fe6e83c190f7f8b3a2d70aef8 100644 GIT binary patch delta 12779 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 diff --git a/tests/test_deploydiff.py b/tests/test_deploydiff.py index 404ea72..6ff30e4 100644 --- a/tests/test_deploydiff.py +++ b/tests/test_deploydiff.py @@ -481,3 +481,68 @@ def test_rollback_pulumi(self, sample_pulumi_preview, tmp_path): runner = CliRunner() result = runner.invoke(main, ["rollback", "--pulumi", str(pulumi_file)]) assert result.exit_code == 0 + + def test_preview_exit_on_destroy_no_destroy(self, tmp_path): + """--exit-on-destroy exits 0 when plan has no destructive changes.""" + # Plan with only creates and updates — no deletes/replaces + safe_plan = { + "format_version": "1.2", + "resource_changes": [ + { + "address": "aws_instance.web", + "type": "aws_instance", + "name": "web", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": ["create"], + "before": None, + "after": {"instance_type": "t3.micro"}, + }, + }, + { + "address": "aws_db_instance.primary", + "type": "aws_db_instance", + "name": "primary", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": ["update"], + "before": {"instance_class": "db.t3.small"}, + "after": {"instance_class": "db.t3.medium"}, + }, + }, + ], + } + tf_file = tmp_path / "safe_plan.json" + tf_file.write_text(json.dumps(safe_plan)) + runner = CliRunner() + result = runner.invoke(main, ["preview", "--tf", str(tf_file), "--exit-on-destroy"]) + assert result.exit_code == 0 + + def test_preview_exit_on_destroy_with_destroy(self, sample_terraform_plan, tmp_path): + """--exit-on-destroy exits 1 when plan has destructive changes (deletes/replaces).""" + tf_file = tmp_path / "plan.json" + tf_file.write_text(json.dumps(sample_terraform_plan)) + runner = CliRunner() + # terraform fixture has a delete + replace (destructive) + result = runner.invoke(main, ["preview", "--tf", str(tf_file), "--exit-on-destroy"]) + assert result.exit_code == 1 + assert "destructive" in result.output.lower() + + def test_cost_threshold_under(self, sample_terraform_plan, tmp_path): + """--threshold exits 0 when delta is under the threshold.""" + tf_file = tmp_path / "plan.json" + tf_file.write_text(json.dumps(sample_terraform_plan)) + runner = CliRunner() + # Total delta for fixture is $6.50, so $1000 threshold should pass + result = runner.invoke(main, ["cost", "--tf", str(tf_file), "--threshold", "1000"]) + assert result.exit_code == 0 + + def test_cost_threshold_exceeded(self, sample_terraform_plan, tmp_path): + """--threshold exits 1 when delta exceeds the threshold.""" + tf_file = tmp_path / "plan.json" + tf_file.write_text(json.dumps(sample_terraform_plan)) + runner = CliRunner() + # Total delta for fixture is $6.50, so $1 threshold should trigger + result = runner.invoke(main, ["cost", "--tf", str(tf_file), "--threshold", "1"]) + assert result.exit_code == 1 + assert "threshold" in result.output.lower()