From cbd7e74b1c086ef0ee9e218cee3c040f495fa703 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 22 Oct 2026 12:38:20 +0200 Subject: [PATCH] NTFS-Dirty-Bit-Reparatur, Release-Skript und Projekthygiene - GUI: Dirty-Bit/Fast-Startup beim Mounten erkennen, Rueckfrage mit ntfsfix + erzwungenem Schreib-Mount, Button "NTFS reparieren" - release.py: Version hochzaehlen, CHANGELOG pflegen, commit+tag, ISO bauen und als Gitea-Release (tea) mit ISO-Anhang veroeffentlichen - out/ bleibt im Repo (out/.gitignore), ISOs ignoriert - .gitignore ergaenzt, versehentlich getrackte __pycache__ entfernt Co-Authored-By: Claude Opus 4.8 --- .gitignore | 9 + CHANGELOG.md | 4 + README.md | 17 ++ app/__pycache__/pc_unlocker.cpython-311.pyc | Bin 22848 -> 0 bytes app/puk/__pycache__/__init__.cpython-311.pyc | Bin 237 -> 0 bytes app/puk/__pycache__/disks.cpython-311.pyc | Bin 9859 -> 0 bytes app/puk/__pycache__/linux.cpython-311.pyc | Bin 9077 -> 0 bytes app/puk/__pycache__/windows.cpython-311.pyc | Bin 6547 -> 0 bytes out/.gitignore | 2 + release.py | 181 +++++++++++++++++++ 10 files changed, 213 insertions(+) create mode 100644 .gitignore delete mode 100644 app/__pycache__/pc_unlocker.cpython-311.pyc delete mode 100644 app/puk/__pycache__/__init__.cpython-311.pyc delete mode 100644 app/puk/__pycache__/disks.cpython-311.pyc delete mode 100644 app/puk/__pycache__/linux.cpython-311.pyc delete mode 100644 app/puk/__pycache__/windows.cpython-311.pyc create mode 100644 out/.gitignore create mode 100755 release.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5644f85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ + +# live-build Arbeitsverzeichnis (falls lokal gebaut) +/live/ +*.iso diff --git a/CHANGELOG.md b/CHANGELOG.md index e96514f..9610b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,7 @@ Versionierung nach [SemVer](https://semver.org/lang/de/). - NTFS-Dirty-Bit-Erkennung beim Mounten mit Rückfrage und automatischem `ntfsfix` + erzwungenem Schreib-Mount; zusätzlich eigener Button „NTFS reparieren (Dirty-Bit)". +- `release.py`: Version automatisch hochzählen (Patch/Minor/Major), CHANGELOG + pflegen, committen + taggen, ISO bauen und als Gitea-Release (via `tea`) + mit ISO-Anhang veröffentlichen. +- `out/` bleibt im Repo (per `out/.gitignore`), die gebauten ISOs werden ignoriert. diff --git a/README.md b/README.md index b7dc155..8a719b1 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,23 @@ sudo dd if=out/pc-unlocker.iso of=/dev/sdX bs=4M status=progress conv=fsync > Der Container läuft mit `--privileged`, weil `live-build` Loop-Devices und > Mounts benötigt. +## Release veröffentlichen + +`release.py` zählt die Version hoch, pflegt das CHANGELOG, committet + taggt, +baut die ISO und veröffentlicht sie als **Gitea-Release** mit der ISO als Anhang. + +```bash +./release.py # Patch: 0.1.0 -> 0.1.1 +./release.py --minor # 0.1.0 -> 0.2.0 +./release.py --major # 0.1.0 -> 1.0.0 +./release.py --no-build # vorhandene out/pc-unlocker.iso anhängen +./release.py --no-publish # nur lokal (bump, commit, tag, build) +./release.py --dry-run # nur anzeigen +``` + +Veröffentlichen pusht Release-Commit und Tag nach `origin` und ruft dann +`tea release create`. Einmalig nötig: `tea login add` für die Gitea-Instanz. + ## App direkt testen (ohne ISO) Auf einem Linux-Host mit `python3-tk`, `chntpw`, `ntfs-3g`: diff --git a/app/__pycache__/pc_unlocker.cpython-311.pyc b/app/__pycache__/pc_unlocker.cpython-311.pyc deleted file mode 100644 index 05b2dba2939af226720a93b166823aac1ce9d452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22848 zcmcJ13vd)?o?p+qXVlV+9tgb{MqnNT=z#$P;wb^Lv9SOP+pKJ_c2Kuq&}c^V%ovyz z8+OCRf+M%`E{=7#xOVEw>y>YHcS_DlbSYl~wy$bam8!LH5$C%d`^T6TH2l(|1JtF7_HvGcBe*6F>?dQUkk$;0*Cl0l}9FHZ2MkG1Z z))qP|M~0)xp>t9)bbNR?7L7}7FG)j*HzoOUXgG~HvBXFuCWT%|#8Xnd-RCa5@6YXR-#-b%1#cw7)?dwGg1^~3b%(|kz_oJ zrKMz2LK30F5mYZFN2Ifo?AzEAO&w!R)N$o_SL-UQI0+F-Q znZzYcNJdA;Vv-n*#1dyUM=CWEc4`%agJ`BC;tvjLE-{)MNop>1bNZ6zxfqR$iHpgh z{4imc{BW~9qyhC7hP}iM*f8|%VsL^R#z49zV8{nWCkCQZTrGNVb>TOUs~bH>1Jr}x z6}a+ZtLVqoi{AlUeYjTRT7hc~u7bEmT!Cw)SbvQhs1nzT4fy59(?(naxHjQhjcYTm zHMq9mT8rx{Tex*eTld!qWdt)CX`WqAcV2p?vooroYEd3U3%vT`;Nc_3j+{C&`0CNV!^dC! z;+IG>T&cN|=`l&x_`$(wJes1horfr*H}bOB{k1b zT9z@r7R80Qu5p&n>k^-<^)^} zBO;;!;|2$l)C-}jk7^=gV;y5dgXuiBwvSzw>+mQeU}&}%O4)!r$r+HZu5$PJ%Ilt+ zo_l=5Jl~Mxn^e9@;hPqC;fKB(KBczfXPZHA=J-yP?^O6sdQ^F%ax$9ZLnL@ zZU~e8Q)_d4Q00RPAAGQ-eZqOux6rW-7vTY7RNbh`@eL~9pzsa(G?e;{(@)K`=lI^ljkw}Q(J8v3wsG70wm-1hI7=$VEE>17v1ZJb8IP53W{vbH_rf9a zGv3m>p^X^ji)t1QGt zt&Q+9BEO12K(y zA#9WnVIv=ejrhaP!`vH(WnUz?yy>1H?zL1=bDT-+<7CsO!*%(W3$bLbj(+ALn+cT`6Jx-1wrB$H0O!4 zk43~wj9%89!_inwvt7zGn)QNETuPJh9)s8)r#Dj4r4&Sp#3)o5QFF$m;S^+aXEG{E znd;-x@Gz-49ibN!X;PlPAQlp~%Wb&G>p?VMcRG14BE_NCWSaX<_4bEk335sls!u$$ z@o-d5U2cOa64rd@5^^*H6)X}%25$Ty8OC`gkvew@DQ=|1+|s2nDn`W#NIFo_AOy$5 zHhB}C_2MNz9w#6INik`N9)_LlHF;a1H1Y<7GSz$c9*ZVZ&?ov?%>DZQ@4T4`#N>tN}Q2NfLv?^#(X=s=72vb8dD|H1^PK9EU-dJa1k@!eh;~}R- zpsk5v2dlZf83{pHgvumkR`f~G_wvdq-qjV>On`LTLZ(#$^PV7KaU_A^;k=lTk&os+#|kMQpsbu1<;YlOk6EsT(g@8HYHGVh%OnN$ zVnjL@L;9o@j_RYNUEW1?Z_%7$bSNcL!)UHvIWj8I*c=+s+&$@3DiN21^rSK|CdCK! zPL)KhiV3fl-%`Z%9AhIciMrmW`Hn@-K$vAo+fyT&C*Sd!mxSHH7={6c7%HwgBBH2y z(@9Cz@A6_|kTtmGJ(G&(gKpeJi)tRaMFB5v=;E;EqkH)S` z(X>nc7!o1m@2Z+9Th^vjt;tobQLENWI6kkcz8RfzUQgUiOgI+!z=Y!_ukfunCx<|$ z`t={6_xXyOzR7LMs$C1stJUVVMTgDX@g)cHDg(~P2H>>XP&GUS7wtm(* zxAo3eg>TOBM^yfZ!XGg`R$4k{gR{XLe^BKQD*QpxM)({2WZhI`f;W_%&jYo$s-{xP zrY704-P80g3=|vkl2Nb;nN~QRGWz(%pcCE7I(88wnnay(ph}LkFqJbhBD543UH?6w8 zDci4fbT71Ro%!~iE<|fSPSHRS4HVJ*pBFMc`q)e#)YRW<&p!43mRaXV{yTM_1(oU( zix@B)8JXx_5GsGzd!u*Cojp0xn-eyu!Ulz%g^q1j{5x{~9jbqaX)J8RSdbKc8_wyZ zeg(Z>yW-Z)sqNFY*`_<|mD;1Z+M{aiQ52S^&gDs2oqA^aso9frCnqX%`~j6epzsGs zCH3C$DmBM*{0Wsmp%hMr{F-Al3FB2VB@Y=AB<3t>cbK)dhyM+McS$zKRX>)t*od~7 zB`hkYx6G**lHx?iIIOlA{4SN#Ejcc&Q94Sc49h#j(aH|dRbEOao4ETlcRV4FMq(e@ zbjIS4;^HHlPu`D2z`I|^Me`0N;={0z%97@Wodfn(w45YCA|UXTB@o>{p)+Oq4cy4^ z3=n~tuh8RjAXhooU-$T?dA=zNGeJ|1-=y-J6n>N5k^US0Dc4jod+HA}B~Qru@N$lC zR{3U3ZgVnZ%*gRuRDO$6I9Uf<#0lbVtXo^q9aclad85J5j7DdbHZe1G8Qp=HB+RiR zYZwFycj!vyiSn+^QaQ>F7}^{&Wu`W-#dug$+CRYiMFTvP5u?oU&b3_1P{BCvDNbGs zlZc^)KtwjgZP5i|hK(5x@xIx__bmupS8U&dEw(=&x@fz|U2=SjyJ$nb4I|t)sduqm zp)#zwVz;DYDB0$Q5$!P07%FvXOWuT9lU-6(PSJqOAItaWPp|+|ZZc&Pg71^5L1mi3 zy&u{&p4qSm6G_dNH)B)S-AnS6oT-l`&%{PT!}!Ha_A!{q<6XW?1BAc4aJ&)HG3@!n z5WQfh4+mt@1+kyuP7l}aYASr?hi zoz%QXE)7Xz^iZb3sCkp;5*Hy?CS>_Ir8P=7JrJ?T`p>2$7e3#`auer`p&#arox* z6VHEj-`}vv*}b(za$l%dg%A*9^YeH5L2%}T7gXT|MR;LBsF~<3{C$OE(S>*E>gK$a zOP8p6#jPgdQ`;FKW)-;EcTZ@Y7h1FHrmJTfbHX-N*ru=(l(V-xVvq>o+iiLoee!J9G|RkA^fPGS$5lvaS<91m8~C8UoeIhG0A2TDo zR%OWkY)EEm7}yu|19mTlB$K4|0rsnEZpQCbh3BinxvI@-)nbS}ujq)O5gI!$1neG;n_0 zX5|T2?D0PX-kK2oyuk^8yl1aCP)5_!Vp+lb9ZN0QSjuUXL3EDWiY8ScmkY>c!R(}) zVYcIY4!!HMhPkwuC&!hRzvL}jkvCki7t)LG11~_j7i}rS1jP7C+ZBhUv<8NY?v%+i zGF~F8Sj%nW3i(jE3pp9Q2Ha-Qz<3O3!`PWM@wj3QEinx9UGX_YE&M2QPSI1G3f7)w zCfQQ$AL^8vy_#v zZN~RA70*h#fh5%xdgKhX?GI%b#dH-W!b% zCv@)aVo7KftgNpjVrjTB5D(S(NOI(|l#D0H#*#V*7)5ahVNDy<-|!~VvL2K#QR0`0 z3=^Tjs`K?Ug{~4I4$oj(bHREdU6M(m&^(Fc;0Re$Dq!GX66VQ29I`e-HzjWqrG4{w5^Y0k*2cNtrtezKE=Y&>OXq~Vv_ydefGX}jn>s2u)troqRiZD>%_J(E|owr}{&ST!nX4ks$3K9=&l?sc)Nh zfZ)st?W)kO2<`W4SE;q@7wS3|RyN#vbCGkb*i1=?aO>>Wk9OzyeJa0C;rHdG5~b#)tnGa_E;y%q^y}<}k1i`akKcJg{|TDo`&Axh zrG6%iSr&q!KH*GupqKx=uG`7ob@~r=IPTW&=-J`;q-q7xu!rc5O5Fd{zUI&-$ETZI z2+K5>9{Ee;8Zw|IViu*Ecp2l}Kfow@mpH7UbAiJe;ZhDodctz<594;ZY22Pgs^y0j z-$Z*6=i~4zVM;ghEV?aWam*-#mE()f8P^h6ZO9Pjyh)!bN&uu`;J3RF1N`1uiqE36 zpMwNp@Hm6Bnz+|_gO`hZ%HSX1=3mcQy~MdiA2=%u6}7l;R+o|dBR7^P{&Unlo_~RfH5WI-$xSi9}t-& z!tjy+tq7VQnnBNzNT-%Uy&vM0y!dLtVoEvrHy~HJMJMlVykD~t2CwQ|&04i)?Zlxx zTuqM5^(*V1&-VZP+kgD++`1j=x*fA;)OEX+b-V9~x%wA!!ZB4irU=LG3)MGYfE~C_ z5!UB~^{TKQ0R!`1or`=ttkl1X^OvpvX|o>638z)zv?82lGI9Zu8VPW9qN_zp>z=>r zzf=2{r$0WO6M9vlR}p#_Y8xhZ6#fX$NGWQ>mFXxy=yCLTxKBL3p8bwbs@L@Fc6_qi zg|NkBVocgg81BC_fgApUm?p+F7M&w3EH5v#mrY3WIYb(Upe}Ds8Ab|BXJZPM!VB|} zGQ@0jv~jShOlmqizE)H(PEn6oCa}F%;-don4CMs`n#*XIWlGD)AaLn2`o`@f`Yj1( zMth?E23WzXv$7P-;=2JrhA7DZXv?yqebqMbN@ZPaWUE?O@CTKl3kPz+vVRwUh+Q!>C>D9DVf$j z02FgRWqBBHAw}JnvOUw%OTMW^oHAtusaF?+i)*Qws$x0G8#Pl0w^s5Er84GWGkidv zqJsPp2-%Tf&pDepTzs!kVyxmB1=77fAvZ4xry*E<$hIWkMv`HFQ7n~jusnv7smoY! zWGKmuPoBK-iD9Y^q5qtU^d6BCxsxnH@=rlZgvIA^ONY&&n0e`ss+IgM_~}*d!8eu` z7b=9CyC&b9uUMs2tSZWseL3N{DjZjY<4n||SbGQs4-lxnIMtdHT2!G$5n2p9-8Xyk zqgRyreK>Q%epT472>b8*8&&_Bg+S{Rn|`Q zF4{fbXKl-&ezrgR+VtUE{nniDv?@HU2v6sW%P-n(tp~nz*eefWQH3ZGHg|bkx4U@{a zEzQZhnHj!#&xG#TrBM=STZ(zW9|PM9WuYs40GAf`4171owhEcL63rB8cORC5$t7He zBgi_`Ho84o9z!tf(`P6ZLjD;MQVsQK$h15%0?SOxdy_&m0}CrHBypSVaPm>yC`?1R zlNwgJk0|1wgTVM!HVrF;Mb6`GU3T`gXCv=J8fnLw6FO9(LlHX2L^nQlLGg!hF05WV z>AWQ@1nO>8-3zRn53I{ZrdQ+w9crLM33TB8y+G@Hpmlo3+{;R!H5WLf1`a6!5}NJ_ z!FeH=6GAZPDur{=kK(X0l^gyHIpjfJ4k63Dek`{lx75;0kq;KFq+YA+;mGfGcqV1) zaq|c)$`V#l9)>Do^gU*Dxn|s@V=?}wMf&j=B`}Oj;NZpaZc-?IgA%wfzROFngZXrS zefeEjLo9n6wKZgyLa)$OVow$IGm|Hz=uNFErZ`@*##-8r%#djR z9(#H+PqP!3L$^s=H;np=lyFH|JwV(wJI4}X1=RiWq@uKMHJgU zcZ8u>j~eJv*hw~{#;NFh^#-MS!~NhoHP|^@{ZVr+xK9o4v%b7z?o`Rk4}zi6C{~mV z)V=HdkymLtkqewu11FWhNoKHmoK>p(ueaSv-re=tE<@pboK*_G8_gtseB<@6=G=;P z>WXzr4J=J`=J-P@e@H2uOuL-!*>NbueH!u|UI|0dn!`1Y&uUx<%NNjFGFcOc%<=$* z@efbGyaZyJo48Yc&>9!aKX%aua}MnRKrFK{2lLL5MwaFfPmqF9!sSwUZ2pEWy3E^; zDLs=RoPF{+RGj%>$WesCF8LJQpfxr5G?7<`41na8-N zx}dvcVA(b_&xdB8Q#QPI#|M(Vl)a>!d<|Paa{TKm|GL7zP7}<3k8hplTW3!z8(+WE z1TxJ}^U6zj+?wOxQ293${tb!;u{1c(2WK0Ubw{(Ov!~}W*f5ggpI7r4uWtlERvI!fa;(A$pX_c~|ikTSH*Vn*1UC{62_T$}_LF65N)qw$i=- zLhn2avixeR-P=&qsZQjCld5o15l$K_u-~29Hg)0lGf(6eDr6L>9Ol^Qn$SNKYO@xs8L&LlCxX54WJq?>J~D zqL;IC*KMU`xbDarFPQ@()!Z?RtsU3+QbB=vawT~&saGNANO2u;Wy(&~u#A43DKu1^Urjofd5CwDn^BQ#PldT@TlX{E zhraH5$6qvdS3B+sF8sb*?ZS<_^$rR@v#O`ofz1(`8*9{O;g0SWBUtE&)2?Z6ia}E} zepYkqD8;r2$f<(B`xnSYZ*r!vQ%nHrS3Jl?)o+9c%C$!FyE(D^w3TX+N_T~AXUZ| z6Z)XRhD^OnAC-;>nf)3e*= zY#+OG!2@dWKpu)RwO!CyS(Yg{Nzx{pl$AgKR%_}*Y2<%T-@=Ym+s@6J~%%*%gD88C&JY9KTFI?I%q;h7Ts@9^M5-5XQBOOaSJW7ct@ zg9mjk*N-3i&hA1x{~z2*E`ksUl$-X*9|LOcl=C)hd zOG~>4Vs9_fEFu$-%=w;!+04+R#jmRnK3-+%T(>7zrYg~s`J-qp60weG6&wl}WWqW! zAG3onFiUf|>|jE<146kM=wvM$*`H89v*S0@JYj#Lq%u|3Eo^05+SsbuhKzkfmuB09 z)qf{BK*vHNZE7xa8-!$pnWd#EkN(4>#-km5plU z+6An&1rN?fJ{mSexjRX0CaO9{n~6X%`#~q{4=O4ry7VPGe>E-G)fV;7ft>J~D!ir$ zujvtrm{Zucvrnn9oC_=JZ@r?_Jfok08SgG+cP>Nx z4T|#rQ=evYea_yF>j;ty?pA}l3-zft)u$r@%g3A8uMO~Uo4~^=Oz0*06lK$GfCWH! z6?gRgsYVmmQUN}s27Pid;tAw}ol1T2oa_F$SJ{0+KMiveEKWG#8=#8UgAa8F_n>uD4K+N(D0EzFfLoN?gG!}ih_DvHk? z$my{$DTMFAWzw_*OFE0`V;0RB?U(X?|$ zP&e+q1 zJF|NrET-hdWthZP<7SfPjBXN)PtAFDE~ ziX?c;cHajPL0&uF%7Gj2Qod?z;6Q7YeggEt!RiDL8e*PD#4;^eHjY zt??-_-ynSb#dM6q!rEcN!sEq)aZW^7BNF68SNaH;Mcy z5jJ-FDMW&xOyDomz-5i6Rp!4aHYCMjgM(<|5FW86&P$K9l}9E*#?s`^(7NCdg}+66 zg=-6Z_Lm_o?XHs;W@Yp{Hm9p?k+YJjN*6gtmC>NnwT9xFiO}T2f~A%0^10wiWhD&_ zE^H~VlI>ns>mp|*4fU?W$h(YoI$g(czl;hl*V;wSAOW8X8ncx&RJow+T1m+7f@EqX zYk1eUkwqD;^SWqqmeB^gYb9p?*AX_ZS--JCtU03blm+4qDYy?^y4=jR&QgQ>jf(aw zSQc5@hG%_;ylb)qsbz;X9-QvY9U`Ef2)Q%C`;dc=X) zQNFGETU^{(Si%rp{haw*SJo0{X%dtj);Pd18;tspqX`f*2h5*if&OdyASZq8^c*%- zZOpXyw1+k%(_$iI{fys+aC@fuCHi)!B*T{;E*fM5B;6Vx36kVJ$N=9Xv}5&!3TyCi7{Xy z;h$!clT?TNW+2T;&*jf?r=0-yrw9OGJ>F}X?~h*_pS&>dX;3^33suJ_9JG&j301)#7@>`(qXBL9`hzXj1e zDeR_##|>+3y?tpe>0iuojHO4|w;**UMz#}O1f%s>eL7Mb%KsTe^XZ>lJ#s0UlF4wZ z&)HUl@EIeW6YQ7&7k;9}A;N>A<~AF)AK08g43Ij`wpQWRn9m2C_bU5a;Cxrv-vSp< z3g-e>rxeZwZmY8V`G9*``MPt_eadMA8ZAvH*4$k8C42TObBs9evsD5uEu@Ka3KN-m oN2B6sT=3ORY??eZdFt&~ZoQK8wWz)p#o4mx;G9i(${Oka0pZ1V?f?J) diff --git a/app/puk/__pycache__/__init__.cpython-311.pyc b/app/puk/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 6a7ab4674ec04cfc70c5f80d4645db67ee3cec5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237 zcmZ3^%ge<81T)Xw%t`{%k3k$5V1zP0^8p#t8B!Rc7*ZIc7*m*n88n%zTmqbRL-TU- zle1HctQ7py(sDBMQgwq;lk>|`iz*e;N>hsza`Lkib5a$2GV@9+bQMbTQWU~7^HTE5 zi*>#8^GZ_l^r~15^bGY3{4|+vvB$@! J1dCXK5&!|#LLvYF diff --git a/app/puk/__pycache__/disks.cpython-311.pyc b/app/puk/__pycache__/disks.cpython-311.pyc deleted file mode 100644 index b487a41769bf37dfe2b01fa3ca9c60d2e9418d8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9859 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 TqT^eEuf58!uWM@zWAyxAx$4!m diff --git a/out/.gitignore b/out/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/out/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/release.py b/release.py new file mode 100755 index 0000000..e466a79 --- /dev/null +++ b/release.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Release-Automatik fuer PC-Unlocker. + +Zaehlt die Version hoch, pflegt das CHANGELOG, committet + taggt, baut die ISO +und veroeffentlicht sie als Gitea-Release (via `tea`) mit der ISO als Anhang. + +Beispiele: + ./release.py # Patch-Release (0.1.0 -> 0.1.1), bauen + publizieren + ./release.py --minor # 0.1.0 -> 0.2.0 + ./release.py --major # 0.1.0 -> 1.0.0 + ./release.py --no-build # vorhandene out/pc-unlocker.iso verwenden + ./release.py --no-publish # nur lokal: bump, commit, tag, (build) + ./release.py --dry-run # nur anzeigen, nichts aendern + +Veroeffentlichen pusht den Release-Commit und das Tag nach origin (zwingend, +damit Gitea das Release anlegen kann) und ruft dann `tea release create`. +Einmalig noetig: `tea login add` fuer git.hacker-net.de. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import re +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +INIT = ROOT / "app" / "puk" / "__init__.py" +CHANGELOG = ROOT / "CHANGELOG.md" +ISO = ROOT / "out" / "pc-unlocker.iso" + +VERSION_RE = re.compile(r'^__version__\s*=\s*"(\d+)\.(\d+)\.(\d+)"', re.M) + + +def run(cmd: list[str], *, check: bool = True, capture: bool = False) -> subprocess.CompletedProcess: + print(f" $ {' '.join(cmd)}") + return subprocess.run(cmd, check=check, text=True, + capture_output=capture, cwd=ROOT) + + +def fail(msg: str) -> None: + print(f"\nFEHLER: {msg}", file=sys.stderr) + sys.exit(1) + + +def read_version() -> tuple[int, int, int]: + m = VERSION_RE.search(INIT.read_text(encoding="utf-8")) + if not m: + fail(f"__version__ nicht in {INIT} gefunden") + return tuple(int(x) for x in m.groups()) # type: ignore[return-value] + + +def bump(ver: tuple[int, int, int], part: str) -> tuple[int, int, int]: + major, minor, patch = ver + if part == "major": + return major + 1, 0, 0 + if part == "minor": + return major, minor + 1, 0 + return major, minor, patch + 1 + + +def write_version(ver: tuple[int, int, int]) -> None: + new = f'__version__ = "{ver[0]}.{ver[1]}.{ver[2]}"' + text = VERSION_RE.sub(new, INIT.read_text(encoding="utf-8"), count=1) + INIT.write_text(text, encoding="utf-8") + + +def update_changelog(ver_str: str) -> str: + """[Unreleased]-Inhalt in einen datierten Versionsabschnitt ueberfuehren. + + Gibt die Notizen (den verschobenen Inhalt) fuer die Release-Beschreibung zurueck. + """ + text = CHANGELOG.read_text(encoding="utf-8") + marker = "## [Unreleased]" + idx = text.find(marker) + if idx == -1: + fail("'## [Unreleased]' nicht im CHANGELOG gefunden") + after = idx + len(marker) + # Inhalt bis zur naechsten Versionsueberschrift (## [..]) oder Dateiende + nxt = text.find("\n## [", after) + end = nxt if nxt != -1 else len(text) + body = text[after:end].strip("\n") + notes = body if body else "Wartungs-Release." + + today = dt.date.today().isoformat() + new_block = ( + f"{marker}\n\n" + f"## [{ver_str}] - {today}\n\n" + f"{body}\n" + ) + new_text = text[:idx] + new_block + text[end:] + CHANGELOG.write_text(new_text, encoding="utf-8") + return notes + + +def git_clean_or_die() -> None: + res = run(["git", "status", "--porcelain"], capture=True) + dirty = [l for l in res.stdout.splitlines() + if l and not l.endswith(("CHANGELOG.md", "__init__.py"))] + if dirty: + fail("Arbeitsverzeichnis nicht sauber. Bitte erst committen/stashen:\n" + + "\n".join(dirty)) + + +def main() -> int: + ap = argparse.ArgumentParser(description="PC-Unlocker Release") + g = ap.add_mutually_exclusive_group() + g.add_argument("--major", action="store_const", const="major", dest="part") + g.add_argument("--minor", action="store_const", const="minor", dest="part") + g.add_argument("--patch", action="store_const", const="patch", dest="part") + ap.add_argument("--no-build", action="store_true", help="ISO nicht neu bauen") + ap.add_argument("--no-publish", action="store_true", + help="nur lokal: kein push, kein Gitea-Release") + ap.add_argument("--dry-run", action="store_true", help="nur anzeigen") + args = ap.parse_args() + part = args.part or "patch" + + old = read_version() + new = bump(old, part) + old_s = ".".join(map(str, old)) + new_s = ".".join(map(str, new)) + tag = f"v{new_s}" + print(f"Release: {old_s} -> {new_s} (tag {tag})") + + if args.dry_run: + print("--dry-run: es wird nichts geaendert.") + return 0 + + if not args.no_publish: + git_clean_or_die() + if shutil.which("tea") is None: + fail("`tea` (Gitea CLI) nicht gefunden. Mit --no-publish nur lokal " + "arbeiten oder tea installieren.") + + # 1) Version + CHANGELOG + print("\n==> Version und CHANGELOG aktualisieren") + write_version(new) + notes = update_changelog(new_s) + + # 2) ISO bauen + if not args.no_build: + print("\n==> ISO bauen") + run(["bash", str(ROOT / "build.sh")]) + if not ISO.exists(): + fail(f"ISO fehlt: {ISO} (ohne --no-build bauen lassen)") + + # 3) Commit + Tag + print("\n==> Commit und Tag") + run(["git", "add", str(INIT), str(CHANGELOG)]) + run(["git", "commit", "-m", f"release: {tag}"]) + run(["git", "tag", "-a", tag, "-m", f"PC-Unlocker {tag}"]) + + if args.no_publish: + print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n" + f" git push origin HEAD && git push origin {tag}\n" + f" tea release create --tag {tag} --title {tag} " + f"--asset {ISO}") + return 0 + + # 4) Push (noetig, damit Gitea das Release kennt) + print("\n==> Push nach origin") + run(["git", "push", "origin", "HEAD"]) + run(["git", "push", "origin", tag]) + + # 5) Gitea-Release mit ISO-Anhang + print("\n==> Gitea-Release anlegen") + run(["tea", "release", "create", + "--tag", tag, + "--title", tag, + "--note", notes, + "--asset", str(ISO)]) + + print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())