From 5c5c35871ddc5a14c9f26fd2b40756cacc718e7c Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Mon, 23 Mar 2026 10:58:48 -0400 Subject: [PATCH] ready for initial release --- .gitea/workflows/release.yml | 43 ++++ gradle.properties | 4 +- project_picture.png | Bin 0 -> 798 bytes project_picture_full.png | Bin 0 -> 17839 bytes scripts/publish_release.py | 397 +++++++++++++++++++++++++++++++++++ 5 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 project_picture.png create mode 100644 project_picture_full.png create mode 100644 scripts/publish_release.py diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..5dd08d0 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,43 @@ +name: Release + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + env: + GITEA_SERVER_URL: ${{ gitea.server_url }} + BAGTABS_GITEA_TOKEN: ${{ secrets.BAGTABS_GITEA_TOKEN }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + MODRINTH_PROJECT_ID: ${{ vars.MODRINTH_PROJECT_ID }} + CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }} + MINECRAFT_VERSIONS: ${{ vars.MINECRAFT_VERSIONS }} + MOD_LOADERS: ${{ vars.MOD_LOADERS }} + CURSEFORGE_GAME_VERSION_NAMES: ${{ vars.CURSEFORGE_GAME_VERSION_NAMES }} + CURSEFORGE_GAME_VERSION_IDS: ${{ vars.CURSEFORGE_GAME_VERSION_IDS }} + RELEASE_ARTIFACT_GLOB: ${{ vars.RELEASE_ARTIFACT_GLOB }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Build release jar + run: ./gradlew --no-daemon clean build + + - name: Publish release artifacts + run: python3 ./scripts/publish_release.py diff --git a/gradle.properties b/gradle.properties index c384d0d..40c6c3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,9 +30,9 @@ mod_id=bagtabs # The human-readable display name for the mod. mod_name=Bag Tabs # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. -mod_license=All Rights Reserved +mod_license=MIT # The mod version. See https://semver.org/ -mod_version=1.0.0 +mod_version=0.1.0 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/project_picture.png b/project_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..38934cbe27b2ea9a79e345b3a3379f0249aef6d0 GIT binary patch literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1!1o(uw0_p$%{~tSc%-r0(ySw{WT5@P;sGFPL?AZ$=EEFm#nySL}4=v9B z&%%(JQs7}E^XzEMv}x1gR|DNig)We7;j%q!9Ja}7~2)icyHH0BQ?S^&C^qh zfdj~4WsqWIWncudynt95$_9B`gOM35&IDu|GBPoMER6!<%yt&Acn**a0-wql8D0QG z2S%gY%D@1WoWRb&0#s{YWNg5=0Aeo4PSynwv!(&rAixAPnF*{i$kGDHg6c9fFaXKM zyiNSY`C`>>U<@RAx;TbJxWAopG4HSfkE@|?HWTORWB>oZPT^pioRb-5iVP0_Pgvf59W9{c!Z62D}VqD0ewHH*K;-zr8H zSiaX-5G>g6@~=QZKRdU;MDN+}o-NT8Y7t&NY@A={e4Q>sF3?8X$-KgyU8XS(A z3({WspJaM2Gu7&OmgB;y3|`x}o@h#4cqw&aQ>p5%bxsRTCiu*0axXYNFKj{(x52pv zDO*VcZo6+DjF*)heq}kAdoaC@oM3i=>k`|N2DSy-J<%&zZvWYpq_ku2i z?Dlzm3uKbMb1=MQddwMe{YGtvgS|uF{(|0=4xumCrVCm)9S}@+=sy{C-)@;!q{3mQ zsS}lvn&c>Se9EYX7csRA=ZRj+wQ;bVsX6YUS^)!iitywdf%8KNIacpt8OTB;d;-*V_iSL7?)4lY0*j z1K*E(-?R1ufljyl-q<>zpB@1x&--gx`I|yr{9zA$ok8~>KJoIGGSo8r^M;g+l#I;l zV?2GpMYYaWIxado;-F)|m%|`Fw!HRO|KI=fQ$e7EAkdqmphLhl*Jb6dv%UX&yT9)_{QmE4r2@yl zKL>!84*cy(6J;gf=s)g~xqkikuSaf-_*EqMs(?V6wmO=(&4L^#1p8Qd9GtktVr4C* z;drI0>`W#urx`xl5m691_(J*;q4m~*;b(_QcYbLdXnFpYSLAj6)6NrLE*`SJbcS;4 zMasGOU%Yd7?!2+Hs^`4;XDX*k#fQ7>a~m7?cgeH4;trvdDxhx?s0LZ7?_9HfNXA6%4px>XCD zBI^IWKBzUu_K@QLw1I*R{^nfb5VB-K7hOOK1$+}tIj5@L$CdtBA!e$Q4Ca_?8| zgFoaZGJ^~0%tv$+vBN7msJ zjNo()3M+sxy51-Oqu+$g-$$`NEUq@)zDhHN^02m8* zFdAQF0go$lV8Q>=%$1+HZ`YH(TT*cmdu3DZEfT>$u`i7MZ&662*E!E@QR-C0(<>x!G+GyFbo33#YXP zbgbLMSkUW})H{*m)DL5R=|DTOX>r&WXev33u(c}Nfgxy=F34i-SgoxM()nxu<7FYiF zRqqc+8wgIy`!sxbe9X->3Mto!y`aODwlXCS$jx-ZlnK6x(SFW4M~_P?3uzjWzeHe`R@iX~vbuTF-LOBaxcNyL^07V+vn#yNbm5}DoWvc5CHGP~B%lwP!yz}iko zRum?xV2A!g?se4$gxb#BV(996PQ1}AF3LwZ`Jk3#T@a<5*vZT z9$m#Ex}X^bfbHkok01$JoqXmrC3zj@aZO6s!q+Wol%&`!HH7qxL3k9BopdF>j>>51Ji7)E^Gb!-8tv)+!Q4s|@Y z;j3GmoIat3vLVw!p-Nk4AM%l6A~no4jf@5qj@z%Sj6DxFas3sDT8cY5#abMbI=?ldgR1^9c!=E6d){FPMu_?zy&lizO*{?5)4 ziu$Oq+j>ekD1o2$Y_2747ow1903N7RJmsPalDW&8`|i}z6i&bGJ6C?@XuEBQNu0xF z{aN~VAAXDbQlASpnCVz@6{2tVXvum%uG-VJ61QW~x>~H#;_QtAy7_E1H+%o*%7^Ke zE<4>Pl{k5WS;B{oghO7_Ll=h#^H0$wb!A==?Mbr%;n$;u8Xd}tr5jeh^Ftd3MV#91jPlfQI~H!X-3agB85|$@xaoV%^1>~y$hDMs8TbxjL}Li( zY{Dv-x&9S3v4L#xvMQZ^5cKJlkbBF$&G*?#3U?z9Y5(})xi)iL)10Hjb9+Zg)~EO~+6!>5qddM?r$t@fiw$zVzXUt}fgJH8e+XLk z3CK~x`vNzXN)VoQ{(qt{yJ;O-Tg~>p68>N!eBvl9KwlQiE!ARY2?D8}`77}G@4lp< zV5QcV`tHearGP=dazJMA#{%qLkr8vHcJbDJD{|Wjhz)pA=nTupy9DrE#4MP# z)uCm+w+&%II<&N@W$~Snir7)ewkzua5VT2$_cy6Y-Sn+VColaqg!P|>%555?-wzCx z3%UIPh^{>6vvxxfG+?L)hu!znSZgt}xd(T$i8~bF#}4p*VY}ARwFvMw4FJ4UHwWSQ z`kQ~u*N+o7WIF9EBfr~OY7A==4*g3MB;dSTFk)jUBI+n2B3@j4TjP{iRXLYgTgB^_ ze-?`-Ryx1EBvO3aOv^m4iI7#?H(yz8fYaBw{?=ox;OnHvcyjNfs>wsdXhVf=rqI5m z)GVV&YNN)Z$g|%?j*FeoF}8R68R#iv>FThhS`g#U?$m2BDaWcjlox*cDouk{{hmvi z(M4{YDQAN()2o~8)1u#5^;!4S`;Tzm8Q{EgOt_u%PHeH8%MfiwY2`{KacdG0J5ivx1=(~6`?COm+br2#LYCL*G1V`4sQXk z2}J}v3ZcUpDIf4oi08tZ##J6W1uXzyk|N-``|3U4ovRCXMkSuPh~<0kz7AF>y2~+5 zjV;pUeWcjwpvK!Il3QU@YBs8G(pUEgW%Oez7EO84 zA@>-_^|qOfMpKFOF}%WI$f~bhD}TBKOFw(&5^Eb78*};> z1inh~G3F-HwR8A+1%7bsxq=m?if42($xEX(g)ZOFagi{6%u=hYj*)vqDsdj;254*O7@6ML+38#7;pi7J9*9HxT^kjCQIOt9gVVq& zwkV5CdWc_O^x!<~+bxe?Lsg0p5C1EPnH2(UH# zD0OWRzGi0mAw6@nWmkd%N49>`5u7`&{9+*;#q zl`{}mPv_h0>x-=p`Xq22eYJvev+6OZxzRhBWV`eXZtsV8o6sKkVx#KetZsJp^@c0n z{KkuldE-NYj0JEY4ecOK6aCyjZtMv2zH3ZEQ->cxlg6$k-|T^quTVf z8f@u*?wYBFbVKS1bLH!od9R*9XA9&tIo%%>k^~fJ;kN?aB&X;aJ)|v}Ek*3N_)#ii zF_`Q!qZZ%M_tSfK>nUkAyM{xlRauCE!}W^-+@!j*LY${SN(Z#lF-87$CvwWfZ(sW5 zK{m+rt=>Q7Eu^#H==akiv^Eg$L#dG-pDZWDHRjgXQteu*(i8ko2>;5`*VpnI%RB|T zPc$x|JsAHqKs2f7EuhEu+IxC$|B1TDp?BI7=4U@$tuUE_M1dh6R9(E+ern!{t=!R% zI9;~t-8gyxMD5~6Vij}RG&IBa7Z5&n^4^(sWx6p`D#lf=yV_35u)j~Y#UI7^$npL5 zMM|GdS&;JPJe{&G6?#M3E=YUpP`!N3IVX=9KzCS7>HU{R6k*XyT+qNEiaCyo)qT}2 zJre*p#x1|H5-{T+A_QRgkW6HOLubc=Hfy-({9FN(31PIQbO+JH@;cJmBfH-XrsUYGs66YphOJ?cOQ!xKz($)mf!e*Icr);BXWG~|hC+b#z^4JBP2R&;HWXKwCp z^uo>68fv!Kn*v8+%p0>{k))k}m&9i}OQNTM!XAzE1O>oDj z1-b}gvZ;1Uaj%3aE~Swn;0ue10=4YxR40}GQu30g%Y2$_&&Rs2fVCbqE`Wk39;>9k z^x}8Xesvy4+;m%Oyw^ES_(eEA(ZPv1sNzmkXWW;1q}sp>@`eK5X*Ye~+0b$F(6`2x z)`paCvqwF@wPetO$`7u7B9{F`qM`z^QdDXCQ=nQF&?#xzGePRoAMX+E@CF0Mj(F)1 z|NX$X6g9y>alSjtWfR^%Hpm#qch+6F-4fkx^%oCB#V$9Ex3;;W*?`_f=n#(!CSDHd zMn$BO_3Yn#yL}@$M*LKa<9)Sl30z#FY`3!Wx^Q3aOt6rC56avw7rXoh#{mk;I-PTV z%MkY3(tM<~XT7*YHixF9CW_IYRFH;sg*0@9{#+zy4CpZ`bOtz`BzaANc4Fuh%J?X= zX#YU7PtZ0A{V}0ON)1*oL3>1Qo{C@Zj!L&Q<;Z9f$B(04M;-BW+=vlNx9Y>?^*~3n zX_rITrXJg!IiGfV3fHpXq0QPuA|3+bTABXk6=NJML;L(IpcNOk|A7O3z5SfiRoL=| z^IAuCXxGOBM(rkG`XUfO4x_^7N^;WB8kvE1CaU554Z7|wsp|!v2--=ARjEY@-7U1~ zo?J|2!Om~jk&fIUE4xh-<kP$*yUtpMg0gZq^U8MxntbnbV=il>yfp6+|pawyB(;aD%3fsWxl~+_)5K`UMgp zT%sF$+uYZyH!-Rx<4RfJdDVs#^*-k}36HO`0YYF>eMv;7*xK+(QiESbjept{g|8S@ zDF3+R2oV3KJ3}J-mr_j5R&qu}_*rLcb8Wtusn^ga1qmqq4-9QI?d{#GiW})vs(Zdt;6umh$RCCp{>GFd{ z5mH>oWp$0tDRr1yNF;f2xHRG6dJw%M^+vL3i%9VY^6OFXL_bd&;r!>YhU^l5{Ng@t zRP9*|^&n_MgX0D~TX8paQDDrbTPR;}V$PivLz=77MLbwJwl(G;$F-UPUD+n6f9;># z?_a~Vxg$;bu0U7-qzj#YScuvrq(U>$AC!uMaLOnje?>WF?gyZ$I>Y9cQQULu^YRXH;=mf}2WJxh1sMLJYJL zB2rxBko_7_Oi_6Cn{zXcn5c3YW1*96c1a*_$@m*^G~aZ!7*=~G$|_2*$>QcQ+rGN0 ztcE1N`$9V8b+drF#e;D(f5IF4D>LqGW*&*Yt##RT{g}V-i=8mRJ3o}^>mL5@*Koyf z9Y_R59+t0)_M$0d_I}6GKBm{}@kgFpSl5a*Ac>{wKtM`-mSGM?{WGi4MtcLbkDrq9}Pi-?m<`s$fxO_ z+E+6~nY$=NIN***3%=lm`rT~lFh9GE_wc<0_yarnW;@FO{r-S98Rb?x%Z>-zVFs;| zFRr_1jJlb-MWt$8`_%f$fJ-rx4p2~wI~=O4MM@1m85mNVqYSb)6DO%)z~$s&_c((y z^JkBv+{sGotp3a_MH+^&cu*R+dhlb`@Yy zSYtU=rS!<9ic@aK2&a-}WC+>0l>VXOSDa2OAHGNI4>{j>N$0(_ws`HvN@61K2y81> z*PQfS@fD|;S(jFPPsKH!lf?8@AosfUbFhG>;H3)+A=H$(Wo$i`3g{oYU2GTG{COj| zdjG&FLSlaJO-AXSbNJFv_b;N#G1^FpDheg&B3RKjH%#%zNn#b;h}Q%8GCCT$$u1lf z-Oo~8f*<+2XGHT%Wmity3#Q6bhD&K%Sthb_LNS@>ZHmqFZlP=wD8=cgpS@ZgT50pL z=A%M@a4*~BtGVKB=}awduum&z9{7pA#prWW)YR29>6`Bd=GYAIw|b$Ukv}N?gReM0 zLTFIi!#soUg+nq+Us0EzAR{gZx7{_U;g$d8mfqQ=*>KiPe7+^75$|Wop!50iqL{PL zw~6_64UADb9D^2DNV5hEqfast%TxxydB~;1qnsb3l6S7EX6@y9MhR;seByk5M5uT8 zB~&rk7wJKWp1IvypM@~zmg#2tcLV-CO6HZ)yI&ghllGyb9$Q1Bd4vaz5u21RDlJDr zQlwx9K~bsne%ir9PL3x)*kyRbm#%c(ehjcQ7Q=$Fbu9 z2w7CZE*%Eg5mM@CX)q_oSq;r{&@PJH;wGE$+_5XgiqA0K{5CIVEgyYD?#O|ar1;+D zpxnkcJS>JC!goHFM;mbIK`wF0*-FmG?99Xg_GBE2skT9lm6+EVX=?Js%sFC$76?$* zh!GQ(-$mv&L*;vA24EwCl7?`)H(spltqX0>@FEZjwmmL>^bM{vDOs1Vgfa$ez$^(C zS`ieAx(2D@7zO4<8nf+KLpZxTWbiVeEp#f(iJ;M)7JJT&PN^X}NCB|&j;2Cp;6^Pp ze74lMbAZU)VLkk9Tm+pul(3_So%Bqhe%M?kc)fcz_ZcbpsA#`wuP#(1p1p8R2AigHpeUl!XR1Mb?IV5D}}xou)BbFEu_y%Z_C zPSS`G;z7DQ2Q|Bxu{OVo;kLcpzSv8+as>z{ZGK0S+s6L^k=w7jiB;iH63KqmpG79u zI~N*tm7OJK{TllH?pI|gW(M?8Hph==938~hOr)w0Og^k<%$VL4iGFuVko$R`E5N*O zCUq;P7`Lg&gu5>o3cBN~3kwT9Gk4YdRkjX-sO2B`et2bE6%or%9#2k0Zg8h?IxR}U zkfsVv;qMcaUf+&z>Gnh?VMIii22*>79$cJfewv+<^sP2KfDV1{7H{mj4%q%ruM06q zx70FwRv5Z<6Qd^n!=8HDW#PW@TOhjZ>odWpntr!L9S3NUM*T~2xtW&9~fWSP#pdA!I)hnqta6$BK@YW zW@_ih{GRXAvuXMfpI#q1{_VNVwMW)i+PWUN*671a^rlIknP)?gcaqB?mwk-b$#qLglcd03DRE zsm)q!qVn>YoRI<3NlXO2ZXqX>TDJwrqs!R#tqTc?h$-x3*m_%Aa3Kh;$9L7=L%*JVH_sxZn%nc2(Oqc7N&j~SifY{byuVz0D*uNvY8Fg51QEo~`YY$BAK7}xsEGJn4G>GJUDts(v z87p$?_MJBzP21Tn*7fN~!u2SvnJ=q2eLo&K?vs$+AzFvhFlVdo%8q!2wJyc6E>`Bn zxjP#;bzOfMRcR-M3|}H;^>!?Wuy5-!m6z7%06*lNMN;YcM8*D2 zQ&r+5t30VLnS9qqdcT)bjs#q$3*|Fwd**$-*v?dD_4;m#LuK}^Z6Ox+Pd90{?yDRG z1)XcR_HFR(6RR>9-oA0DV;#B;CCt6qP1Lbpdz~DNa!lWFyT6QhEL4qeCWwE?WZFc{ z73-&}1vB){90cW@12l^_-@GVumqd)o#qwwM8yL$N=(S-{B~!dIs#}j!hR#W;XDrI9 z!04sK1&gDgpv8DOxU`82@@o1Rdl4o0#Fo<|lsC1ZShuLrMrYQme1HG;N)E>ZB3MAZ zm0?f&gAJ6!$#De^OF!kBQ$Y~2tQBGJ-BdOv?+~ulRCY;>0b%WmpxbW9z-d#MtKg4r z`}1)vDR!9el(EeTDbUn$-v&~sbzVxau->BQ$E{{5o7pes=Rr^PfRNp7h4k?b%+Oq4 z($-K6sY>r5k=%ww2j3XrCC_HymM^uwT-wWoHa6n&k}KUH64#iCYDN*4QY<_#R=;hq zaQ|5Fr`SD)K)>2WpaJM-2yfN7#tP#X?uOu4lAo=!D%ix$eH27Gb`CH`a<-@U4T#$%kOn@a*=s;E@6#u z=;@UK?y9dOxh&*%E64BRoYe$km|+rapGrI1sS@fs6A?WbypWA`VDtjIg7c_&_wFU9 zSp>)4P9JS=su0W++UeovRRemZzL3mX@KXv^Ma*&RPiYW$gpmgI_%E#3Vjp$L{zU&i z#kbzl{6K z+1n|^-I&XtKJBVntJv4=t!W8A9&R1QiPzb=JfC#n{V6#&QcVeIv@W$Qp^v#$-BeM* zf2F)+$EmAslL~VjBmubYsE+JisN4yYK}z$)Xt@o4)J`zza&>ietg!lx2!go(?nNy< zLg(xfT4cktBi!yE2l5LwN7BwV)iBPQ?qe^Ly**5LI&&{aFvu6vFP|Z!CjNArNxR31 zymc`~cd_jA2rb_dQR&QKX>QE^!QJHaZ&Xlle5nL|{dt?@Lb9nLMi_}|=`pK%;xad+ zW5YMl!EppMp$F)aJqSwP8&BA}mexQQ%QK5`TQE(_l=1Os}TT?Gz9pejt6OZ_8MP_H>~t^=Yod%E;GBb%;yyLN^GkF0L%^+ zap0w5P26yV9)4$P0wpH}bvgpdL0CFcy%py=e|5`)Rkx-LP?B$F^&trrQgOPTqZfaa zl;?EAQ-i@X*@+fd*Cz~Wpma~W!1a}+!%@Bm2F0ho_`$f`o5>K(l`&sob>6q~C2Ug(vYzH7m6J?Uc~YfkP^ zf*-R^Oz9=hbxH4M_CGcM4r`LDn(Mw>qqbn*ytf)!dKe(*005DCY-z2Aqmk06a@$Q@ zWC^Uz{nLagkQ__KpcT)1KG_y%9{y6&dD^WJk6>u0p1I4UJn!8wt6vnH7^by8Aa*_g zDe-8hW5isW#-H8eTy-!Meg$MLP+0!;BE^b1fEn=Y@+8kbHPi3SwfdoKby``wdr&eE zaRf5G>EcE(xb)&WS9EHGa}`zC<@V*`cFx>DUJzA!O+rL)8|S-q;{*Q$+Ot5tgFKRh zb5VEDH5-whGOLGXT5lST7thzM{!Snb0hDB)`l~|P@E>LxQTDsLl00*c^~#~UF3?5u zw}}~oS$Ny{)D|yp`FDrjPLWE^Mkt8LxY8pRJ}Z}4nR!0H+l2Y>>Q9ky1IXL5dHX(T zyh2C~SSm`Nl51%Wzn^q(Gu0g_e(8{Txku-v7?*2X4qwDU&1&*q^A~WQDUCTKT)aAK zhvf(0fKpWjE%TU+w1_E0WN-tPqOcnS$gR;Up?5_{J&q2Y5-W+`$V+uGAQM9rXjID8<% zSgUnFc>(nVK=m02B^iy|S;Ah{m2LVb2SC`3^Pmnq&|y!-Csj7!kPS*9GF_k74LQjEnq|-Pd$Wq8>0St+UTx00D_LN5!y`NCUT~1vlM%kRcO^4O zX~gX=QzNx&#jb^b@o)ufpj#D?5c3GXAq}@i1t@2#_s{g3z_JKB-K5q@ z`|eBm4LA=IT^IGvc5Vqa(1Z~14BW1RK?F$f6!G}dm4{BJj5X@ipG9BeiOH(Gr$bDP zS)$b0yKm;#$R;@>>g(J=fQFypO1b|W!Z^1XD`*Cd%toS48BlVZgfN$*m`iuXE+dwb z3@!qB)48j|HD=a~W9H^fQ;_?#q=0w`=mT9$nCVfrbK#A0XhfN%Lx=5nIovKd3S1UL z!R*_Ii`|%^Bz!AwSdt`wrgY#xLy=`B*<scvg0vi24BdPn>BNTF_5>&bakRI(l`g~3>_wf{(eP=y{PB{rsA zauwTLSTbLd3WMh^E~Z&sm1g^$GMZHyu2DI9Q}X9d&59fv?FR`mi<=G2yEHLXJ!(`@ z&a9LdHF~jC6`=9F)GUOVElYA+#>XTcO+p(yE{QjLGP7UN!EU7 z=in49t}^i5w}@83L*7D|NcnT;Cbp)|((7w1No~8^(!^u!WDsb=vLg#q#xX@$FJ?Bu zF}spxE|U`ziV9m-d>e0v;B6)dN2;$R)^Lz&2R2$)9b!&Eq&h{ngRniS>=i{d7HA$wAW;PX9)S zNY0+m24Zs_b^0MtwNg&5j*Q0x{Plz-#^x4c4W;jsmo_6X!PP z^t`8}UQ~3f<^qD=i}JD@3GmrWT})oWO0NyTO!lkSv()DjpA@ZAs?kll9DwP9Rs2GXAj#`+4L=*iF65;_it zBauRH)6-pZ;N(o=nvS~J$3>+Oz$XrC7(n`))WhLXZ!IsI=b=wLhakV95;!%YKJHue zos8%RKE~&?^0rMh)$eRdt-wokKWMTsXmo<1s#rew-Z8(XibFv*soEf>&E&>wQedA1 zklEY<4a2U#ul_n|gMB3_&i7T{oOIG7NwHN{Vr+8|zTuT*t`R#}SoTL{R^{s#R=)U1 zMStT~+e|d%hm$LCY&Gnlpjg2NyAIzOrYSCqo_EK;#EkCj z>xGMAPco4d4xAd)fm*xtM{^z3ZWezyP@{1q8ZE(}b{^~xyQv4*^vxG}w8huqAAc@u z^RlO|hVCIf%{ZtMd+J7$C&#=)?F|~tF!?b=&zV4j#mLWr2SI{`S1!U)XI`VZjmb%& zwyOdz%L=C%xgDDMw4PDypjv$6X4$jl!%si1KFXt6b(X8yz#Bcb{T}?RsoMyWqWnB+ zVJH^!IE|Hqxvd3n9eLKT1hiQKv>919!cCTh*q7K=cceIS52e0PbRxH%)p5hoLqeP9WJfPur-UkGzNM4j%AvU%8G- zom36cZpt#<%(rcJ|L&rUU-B50ujI^qeFC1tHg(T#H5gvTu@Y|-HWS&dbx?@4P3WA9DCG)b z8>sO<@{YW8TbbCP`fx5xw}d_p{p~$)*2D52w;qfH!b{ohv4(MIJNY{rQLe5MO#{y% zzTOKme=km{Q8|du&Q!~qdSzKIWask*JQo`NF>MRahmUi;W<5?Xsh0%9x)?4#m2HlL zTZo+f=K!nvyHX^QQl;{?kf1!6*?vE*#(qCl{!`dsnloo|YH88_V)E`vG z%Lp}iZ&yk*j0f-;mL0pnl!IZq=3ny&)w`b*F+V+vTD(Fb){4nLj4G(V_T#_3Bbpp1 zus@LAsPH20BDyf4F0{(oc?kFOC=cntchVCwfc*OeDdB9%JV!!TihJnTMeVZi7mw=*c{YdJ)k?fQrk^N|JU=H2^BMluiR}O)A&%iT&yo+X zV7fQiK9Ac^Sl&4%^MX_36v3v(L{iYgxV;)t;MuykZGM#zMyv^ z(w9k84@JvU5<_hjsm0aIt)m?D@N;I!Z!+x#PkB$3m>w^5W-xE}}J?|?U{;a^ovtyAi;Ri3_7hq|n53t8ivTj8`V zfPVmT8am=LII-e&++Kvx?i*L%CxHrbTpXlxvnPeNn>UT^Q8N9rS!;?-I|kr5I}02E zCH@-*f>##y?TD9cM<7Y1Kt0+bpd&AVqOW47kTQu=KpY6Kne@=l`>LvFAms1Nh0c(gpKQI;}f7aW}}()wxfctK7p#jXn|vgn$|6GS4-Z zHa>BbL-x+E?_ituZzOr7_9Q+Y$611!;Q`5{%<(bb&7h_BPF-EVyVxo~mqxEEv4@mh z-nFRi#pQ|^3`_FF{0z#s=;=Ai4~^tWFM4&3pQzKFkMuugtUq4eD;BD;-lup~$S}`M zRM}Pf>Xn!qFE}+@86h#w=EGBJNFc*Fr8KN~nIrGv+0>02F$P?YSPnOLRnV4by$v+| zf`+E1V{A-YZnVLZL$nY_c31QXVR?f;29tgJFt?5gXIPJatz(GF#6sxy78G<4@>z{gz#L+vf5 z*!I==6iJF#K3jv{FO%geXUqAMq%G(dJo7B!#f5^JP(C1+6qrnssIZN>))aCn1*7*( z0-2N2!hk+$z^tK-c*N$1J%a43UwAY{(Oe*xC&T5G3PV@CPgbmF8Fa&$kqc6f-$cc2ZTGxqpsva zLYf$n_p|#WK?T(`1{%_pT1{X6mffRUfP4Ntit90fb{!?am*<9yRNouB!EI#TI zd)b55o%KhB;xV_L;2 zd5FAjE1#bCp2z9fboGr;&@BVYujZ3MJP8u6Dk1dIrr(PJfSsxo-!UJBcZ{M!@#%C| zBvTJT>IhJgy1K2!p=qu~+`_oBdTBd3S-$7Ld{q(cWJh{OtDli^$8VB}+jp%g&0&*0 z^OfN3HGb4yD`Jo^jNH9pZk|#(40s<>=`@f^m=i zc7DiiCSn~L6%tdquGZzU;|>X3&rfsM=oSmuqSlrKzbqiaRXeFm?;3!B&YK0;3d|U} zGI`9gsAOC8ZPvO=qjD&N0#>z_`kj<6H004k!rTCz2PmAiM>_xjt){P59YiPty3}Yeem@^Cp7F`ec-Bq@;zNPEpMN8|?KS6Xh)V*WELt*=JZYcgk! z-*$=(UsG~j7cj(NDD4%{u-ZY-K6lmuJBwf-OQHg}v$^F(L%;H)AEHxCOT$+u_H)C+ zaRg4?w4&RODO}CY!q?W_*)<^MH#D1;sE-p~HF-2$`?P!GNcW6Of%+}C`wnHo=elCk zozGS73MQLdo!4u?0_7Xjl#2$C(X+4e3rN`6Ze`fDin-T^=GU@T;8bbT2(= zljC;9+HHE`$Tlz!&#cbd)-!DD!HTOwOjPSkb&E~ zrWv1Q_p2rNcF(Qu!w7DSA8$aBRA{oFA+7@Q@QdkY9=^y6*;w6(<^wew-`w2bJ}-Zq z!|SEx8H3h}{H*!);JyNpNsEapN=S_VqI5R7{`nfDlTi)$j7>3of_v zcMZLg1C*1#vs--@j!CU)>bQwF7IK60m)={!m!S3)33p8HG9InF0LylSn2Xwt7<{SD z-U~Mt<2oKLF?p!bF6Xm_tBqUdT=*FoJyFm5j2OhK^d~n(~Mv2X_v(I1tDc!g+y-Lpj zrj-g%7m7Xg`+3WoB|j8ct=h-axAXeX*00y%P@VU^o~(pI5=}AzB4x;2*S>~gl25O! zQc;-$OEVnvM3f7AU9yKnZB44qX^k=<`C0g^Bx~93ibgkK(3J4+lG;rj1tS@uixS1w zRVgOsW$A&zN^&1S7NsU%-q#Qwr=8#SW+-2Cm-+Xnd?Kh};ct(*Tk z1AWAE1{v91X`k2gey-Ba{yypm=tA8`ePHQZJl}u4blPN8wCH3Gn0Q81yj zx)P^Ez>=PyuRj2qioW%i*Iox|?Jni54%H&sX}Gf0A*_r87TDc#4g0*zCk+>v4WRHz zv-`JqbOC3N`f35~K8^UlECl{ftKv-M_|w=YTrv`fyQM@v-f5ZbeHv>Y$f^|(nNKe8 z*Y?N=CXrd&;B&mwZ0W3QRxm(6;x8m|{kuytxc%%6(7ipfU`8caMe6(I@C|K*|L?XM znreJ9oo2Pee`&XC)w+gH^M(T@0E+QCs;9MK`X+y6@!~&5TW#ALaD$>C*ZlyIKobZH zQ&JFAh}W4=+*)~;NB#HKxhMFPNq|7!!vAtD#6K2b{Ld|)|Hl%Y0v83T(D!Bmb|W~J zmeLPh!KcyK=|6Km@p8c&XU?gowm@gZmT7+)1v(m?uGl{s{m<)W7vR#@S2OfKKbkS- z;6NRW?`yrS;tNffjs{{rbucw0cDbqV3j4oYqPZ9lP&KV5U!F*^^qqOe$ee5)n4O$x z-Mu0haFxrJNNf!ke8jxSmC3KXJyxg)f75FKoe9qfn(5f~7u8oCe#;MDjW(DF%H|OK z=jQtuf<22VkNmJVe6Fg;_p+M@1!@?L%u`E&v03GOA7~xM5#xY_+ZeQ=?`Xn?+B+tA zb`#ON1?k+^GyI9T%%wN^vo+k%!)wO)A_Mh^b@|G2QzA1h>kCVF6XN^*wuNtvV6*AD zjG&vtr`H}`sSbZb2jBaudhq}F#{-{9%RXfKJFDN9B_QJMKOsiV9u^4UL6_TOy6MtveFoQ*4i57z|k zZk_j9{xcJgxXh3a<~)j<*q6W|Iy)XZk`?_>IRvKhPJq&nc~J}53f4^yN5>9vTIZP$ z|0JyK!H~MK^k{z6qksJp3_l}gjA7(Ke~s>=B&M!j$b+6_H65WU|4W%4Bm?EBu5X>R-g literal 0 HcmV?d00001 diff --git a/scripts/publish_release.py b/scripts/publish_release.py new file mode 100644 index 0000000..1e63112 --- /dev/null +++ b/scripts/publish_release.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 + +import glob +import json +import os +import pathlib +import subprocess +import sys +import tempfile +import urllib.error +import urllib.parse +import urllib.request + + +ROOT = pathlib.Path(__file__).resolve().parent.parent +GRADLE_PROPERTIES = ROOT / "gradle.properties" + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def read_gradle_properties() -> dict[str, str]: + values: dict[str, str] = {} + for line in GRADLE_PROPERTIES.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + values[key.strip()] = value.strip() + return values + + +def load_release_event() -> dict: + event_path = os.environ.get("GITHUB_EVENT_PATH") + if not event_path: + fail("GITHUB_EVENT_PATH is not set. This script must run inside a release workflow.") + with open(event_path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + if "release" not in payload: + fail("Workflow event payload does not contain release data.") + return payload + + +def env_required(name: str) -> str: + value = os.environ.get(name, "").strip() + if not value: + fail(f"Missing required environment value: {name}") + return value + + +def env_optional(name: str, default: str = "") -> str: + return os.environ.get(name, default).strip() + + +def first_non_empty(*values: str) -> str: + for value in values: + if value: + return value + return "" + + +def split_csv(value: str) -> list[str]: + return [part.strip() for part in value.split(",") if part.strip()] + + +def derive_release_channel(tag_name: str, release_name: str, prerelease: bool) -> str: + text = " ".join([tag_name, release_name]).lower() + if "alpha" in text: + return "alpha" + if prerelease or "beta" in text or "rc" in text or "pre" in text: + return "beta" + return "release" + + +def select_artifact(mod_id: str, version_hint: str) -> pathlib.Path: + pattern = env_optional("RELEASE_ARTIFACT_GLOB", "build/libs/*.jar") + candidates = [ + pathlib.Path(path) + for path in glob.glob(str(ROOT / pattern)) + if pathlib.Path(path).is_file() + ] + filtered = [ + path + for path in candidates + if not any( + marker in path.name + for marker in ("-sources", "-javadoc", "-api", "-dev", "-slim") + ) + ] + if not filtered: + fail(f"No release jar matched {pattern}") + + version_name = version_hint[1:] if version_hint.startswith("v") else version_hint + exact_name = f"{mod_id}-{version_name}.jar" + for path in filtered: + if path.name == exact_name: + return path + + filtered.sort(key=lambda path: (path.name.count("-"), len(path.name), path.name)) + return filtered[0] + + +def http_json(method: str, url: str, headers: dict[str, str]) -> tuple[int, object]: + request = urllib.request.Request(url, method=method) + for key, value in headers.items(): + request.add_header(key, value) + try: + with urllib.request.urlopen(request) as response: + body = response.read().decode("utf-8") + return response.status, json.loads(body) if body else None + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + return error.code, json.loads(body) if body else None + + +def curl_request(arguments: list[str], expected_codes: set[int]) -> str: + with tempfile.NamedTemporaryFile(delete=False) as output_file: + output_path = output_file.name + try: + command = [ + "curl", + "--silent", + "--show-error", + "--location", + "--output", + output_path, + "--write-out", + "%{http_code}", + *arguments, + ] + result = subprocess.run(command, capture_output=True, text=True, check=False) + if result.returncode != 0: + fail(result.stderr.strip() or "curl failed") + status_code = int(result.stdout.strip()) + body = pathlib.Path(output_path).read_text(encoding="utf-8", errors="replace") + if status_code not in expected_codes: + fail(f"HTTP {status_code} from remote API:\n{body}") + return body + finally: + pathlib.Path(output_path).unlink(missing_ok=True) + + +def gitea_headers(token: str) -> dict[str, str]: + return { + "Accept": "application/json", + "Authorization": f"token {token}", + } + + +def attach_to_gitea_release( + server_url: str, + owner: str, + repo: str, + release_id: int, + artifact: pathlib.Path, + token: str, +) -> None: + asset_name = artifact.name + asset_base = ( + f"{server_url.rstrip('/')}/api/v1/repos/" + f"{urllib.parse.quote(owner)}/{urllib.parse.quote(repo)}/releases/{release_id}/assets" + ) + headers = gitea_headers(token) + + status, payload = http_json("GET", asset_base, headers) + if status == 200 and isinstance(payload, list): + for asset in payload: + if asset.get("name") == asset_name: + delete_url = f"{asset_base}/{asset['id']}" + delete_status, _ = http_json("DELETE", delete_url, headers) + if delete_status not in (204, 404): + fail(f"Failed to delete existing Gitea release asset {asset_name}") + elif status != 404: + fail(f"Could not list Gitea release assets (HTTP {status})") + + upload_url = f"{asset_base}?name={urllib.parse.quote(asset_name)}" + print(f"Uploading {asset_name} to Gitea release #{release_id}") + curl_request( + [ + "--request", + "POST", + "--header", + f"Authorization: token {token}", + "--header", + "Accept: application/json", + "--form", + f"attachment=@{artifact}", + upload_url, + ], + {201}, + ) + + +def upload_to_modrinth( + artifact: pathlib.Path, + token: str, + project_id: str, + version_number: str, + version_name: str, + changelog: str, + release_channel: str, + game_versions: list[str], + loaders: list[str], +) -> None: + payload = { + "project_id": project_id, + "version_number": version_number, + "version_title": version_name, + "version_type": release_channel, + "status": "listed", + "featured": False, + "loaders": loaders, + "game_versions": game_versions, + "changelog": changelog, + "file_parts": ["file"], + "primary_file": "file", + } + print(f"Publishing {artifact.name} to Modrinth project {project_id}") + curl_request( + [ + "--request", + "POST", + "--header", + f"Authorization: {token}", + "--header", + "User-Agent: bagtabs-release-pipeline/1.0 (self-hosted Gitea)", + "--form", + f"data={json.dumps(payload)}", + "--form", + f"file=@{artifact}", + "https://api.modrinth.com/v2/version", + ], + {200, 201}, + ) + + +def resolve_curseforge_game_version_ids( + token: str, + game_version_names: list[str], + explicit_ids: list[str], + api_base: str, +) -> list[int]: + if explicit_ids: + return [int(value) for value in explicit_ids] + if not game_version_names: + fail("Set CURSEFORGE_GAME_VERSION_IDS or CURSEFORGE_GAME_VERSION_NAMES for CurseForge publishing.") + + url = f"{api_base.rstrip('/')}/api/game/versions" + request = urllib.request.Request(url, method="GET") + request.add_header("X-Api-Token", token) + try: + with urllib.request.urlopen(request) as response: + versions = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + fail(f"Could not read CurseForge game versions (HTTP {error.code}):\n{body}") + + matches: list[int] = [] + missing: list[str] = [] + for name in game_version_names: + matched = next((entry for entry in versions if entry.get("name") == name), None) + if not matched: + missing.append(name) + else: + matches.append(int(matched["id"])) + if missing: + fail( + "Could not resolve CurseForge game version names: " + + ", ".join(missing) + + ". Set CURSEFORGE_GAME_VERSION_IDS to bypass lookup." + ) + return matches + + +def upload_to_curseforge( + artifact: pathlib.Path, + token: str, + project_id: str, + display_name: str, + changelog: str, + release_channel: str, + game_version_ids: list[int], + api_base: str, +) -> None: + metadata = { + "changelog": changelog, + "changelogType": "markdown", + "displayName": display_name, + "gameVersions": game_version_ids, + "releaseType": release_channel, + } + url = f"{api_base.rstrip('/')}/api/projects/{project_id}/upload-file" + print(f"Publishing {artifact.name} to CurseForge project {project_id}") + curl_request( + [ + "--request", + "POST", + "--header", + f"X-Api-Token: {token}", + "--form", + f"metadata={json.dumps(metadata)}", + "--form", + f"file=@{artifact}", + url, + ], + {200, 201}, + ) + + +def main() -> None: + gradle_properties = read_gradle_properties() + event = load_release_event() + release = event["release"] + repository = event.get("repository", {}) + repository_full_name = repository.get("full_name", "") + repository_parts = repository_full_name.split("/", 1) if "/" in repository_full_name else ["", ""] + + mod_id = gradle_properties["mod_id"] + mod_name = gradle_properties["mod_name"] + tag_name = release.get("tag_name") + if not tag_name: + fail("Release tag name missing from event payload.") + version_number = tag_name[1:] if tag_name.startswith("v") else tag_name + version_name = release.get("name") or f"{mod_name} {version_number}" + changelog = release.get("body") or f"Release {version_name}" + release_channel = derive_release_channel(tag_name, version_name, bool(release.get("prerelease"))) + + artifact = select_artifact(mod_id, tag_name) + print(f"Selected artifact: {artifact}") + + server_url = first_non_empty(env_optional("GITEA_SERVER_URL"), env_optional("GITHUB_SERVER_URL")) + if not server_url: + fail("Could not determine the Gitea server URL from workflow environment.") + owner = first_non_empty( + repository.get("owner", {}).get("login") + or repository.get("owner_name"), + repository_parts[0], + env_optional("GITEA_REPOSITORY_OWNER"), + ) + repo = first_non_empty( + repository.get("name"), + repository_parts[1], + env_optional("GITEA_REPOSITORY_NAME"), + ) + if not owner or not repo: + fail("Could not determine repository owner/name from the release event.") + + attach_to_gitea_release( + server_url=server_url, + owner=owner, + repo=repo, + release_id=int(release["id"]), + artifact=artifact, + token=env_required("BAGTABS_GITEA_TOKEN"), + ) + + game_versions = split_csv(env_required("MINECRAFT_VERSIONS")) + loaders = split_csv(env_required("MOD_LOADERS")) + + upload_to_modrinth( + artifact=artifact, + token=env_required("MODRINTH_TOKEN"), + project_id=env_required("MODRINTH_PROJECT_ID"), + version_number=version_number, + version_name=version_name, + changelog=changelog, + release_channel=release_channel, + game_versions=game_versions, + loaders=loaders, + ) + + curseforge_token = env_required("CURSEFORGE_TOKEN") + curseforge_api_base = env_optional("CURSEFORGE_API_BASE", "https://minecraft.curseforge.com") + curseforge_game_version_ids = resolve_curseforge_game_version_ids( + token=curseforge_token, + game_version_names=split_csv(env_optional("CURSEFORGE_GAME_VERSION_NAMES")), + explicit_ids=split_csv(env_optional("CURSEFORGE_GAME_VERSION_IDS")), + api_base=curseforge_api_base, + ) + upload_to_curseforge( + artifact=artifact, + token=curseforge_token, + project_id=env_required("CURSEFORGE_PROJECT_ID"), + display_name=version_name, + changelog=changelog, + release_channel=release_channel, + game_version_ids=curseforge_game_version_ids, + api_base=curseforge_api_base, + ) + + print("Release publishing complete.") + + +if __name__ == "__main__": + main()