From 723daaa573f8c7bb3af830ede76a6d553e000add Mon Sep 17 00:00:00 2001 From: Cogan <79615344+cogan17@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:13:13 +0000 Subject: [PATCH] feat(rilis): teks rilis tanpa hash, perubahan hulu dengan kata-kata biasa Owner 31 Okt 2026 (teks r7): "tidak usah menggunakan angka/huruf acak, karena end user/user awam [tidak] paham", mis. "memperbaiki selinux". - kernel/upstream.py: judul commit hulu yang baru sejak rilis sebelumnya (juga setelah force-push fork KernelSU Next) menjadi frasa ID/EN per bagian (penyembunyian SELinux, mount, error compile, crash, ...), membaiki atau meningkatkan. Judul yang tidak dikenali hanya dihitung. Commit yang tidak masuk kernel ini dilewati: SUSFS LKM, kernel versi lain, Samsung, terjemahan, deps, CI, docs; KernelSU Next hanya bagian kernel. - kernel/telegram.py: Baru dan Perbaikan memakai frasa itu; perbaikan pengembang masuk Perbaikan. Unicode fix tanpa hash. "Tidak ada perbaikan baru" menggantikan kalimat commit/rentang. - README, CLAUDE.md: aturan tanpa hash. --- CLAUDE.md | 2 +- README.en.md | 2 +- README.md | 2 +- kernel/telegram.py | 122 ++++++++++++++++++++++++++------- kernel/upstream.py | 117 +++++++++++++++++++++++++++++++ tests/test_release_workflow.py | 6 +- tests/test_telegram.py | 51 ++++++++++++-- tests/test_upstream.py | 64 +++++++++++++++++ 8 files changed, 329 insertions(+), 37 deletions(-) create mode 100644 kernel/upstream.py create mode 100644 tests/test_upstream.py diff --git a/CLAUDE.md b/CLAUDE.md index 3647ba3..5a6f5d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ Nama repo **`cogan17/Cogan-Kernel`** sejak 24 Okt 2026 (owner; sebelumnya `cogan - **Riwayat build terpisah dari log** (owner 27 Okt 2026): folder `kernel/history/` berisi dokumen `run--.md`, tabel ringkas dan metadata untuk Cek pembaruan dalam satu file. Builder dari `main` menyimpan hasilnya di akhir run (`kernel/build_history.py`, commit data langsung di `main` seperti Catat sublevel). Build gagal atau menunggu tidak menghapus pembanding terakhir yang berhasil; tiap root manager memakai hasilnya sendiri. Tidak menambah workflow, jadwal atau pemicu otomatis. **Jangan mengubah Hapus log run lama** untuk mengurus riwayat ini. Catatan bukan pin dan tidak menentukan versi build berikutnya. - **Modul SUSFS ikut di artifact dan rilis: hanya sidex15** (owner 29 Okt 2026: modul simonpunk tidak perlu, "Sidex = simonpunk", dan modul simonpunk tidak punya WebUI): file 5 `5-SUSFS-MODULE-SIDEX15_.zip`, modul sidex15 dengan WebUI dari build CI terbarunya (`build.yml`, cabang `v1.5.2+`; sidex15 tidak membuat rilis), diambil `kernel/susfs_sidex.py`. Modul ini membawa alat `ksu_susfs` buatan sidex15 (`tools/ksu_susfs_arm64`), bukan hasil compile commit SUSFS kernel. Rilis tidak terbit bila modul gagal diambil; tanpa build CI sidex15 yang masih ada, summary menyebut "Tunggu sidex15". Modul simonpunk (dulu file 5, sidex15 file 6; owner 26 Okt) tidak dibangun lagi. **SUSFS simonpunk tetap dicek di Cek pembaruan**, karena perubahan dan perbaikannya menentukan kernel. - **Fitur dari WildKernel** masuk satu per satu sebagai centang yang mati secara default, dipasang ketat, lewat gate KMI, dan diuji HP sebelum dirilis (owner 24 Okt 2026). Sejauh ini: Unicode fix (patch terbaru `WildKernels/kernel_patches`; lolos build + gate KMI di CI pada 6.1.157, 162, 172 dan 176, terpasang bersih di 138 dan 145 (run 40048269498; 138 juga lolos compile lokal); build berhenti bila patch terbaru tidak cocok; diuji bekerja di HP pada r2, 24 Okt). Ptrace (hanya kernel < 5.16), BTF (sudah aktif di kernel bawaan) dan FUSE-BPF (6.12) tidak berlaku untuk rothko. -- **Summary workflow selalu dua bahasa** (Indonesia, lalu English) dan menampilkan yang berhasil maupun yang gagal, ringkas seperti README (owner 24 Okt 2026). Builder: satu tabel hasil di akhir run (`kernel/run_summary.py`); dengan satu build, plus **Teks Telegram** ID/EN dalam kotak kode siap salin (`kernel/telegram.py`: Baru diukur dari rilis sebelumnya, Perbaikan dari commit `fix(kernel…)`). Rilis dari run menampilkan teks yang sama untuk rilis yang terbit, **satu teks per rilis** (owner 26 Okt 2026, "mesti copas 2 kali"): KernelSU Next dan BakaSU dalam satu teks ID dan satu teks EN (`telegram.release_post`), perubahan yang sama ditulis sekali, yang khusus diberi nama root manager-nya. +- **Summary workflow selalu dua bahasa** (Indonesia, lalu English) dan menampilkan yang berhasil maupun yang gagal, ringkas seperti README (owner 24 Okt 2026). Builder: satu tabel hasil di akhir run (`kernel/run_summary.py`); dengan satu build, plus **Teks Telegram** ID/EN dalam kotak kode siap salin (`kernel/telegram.py`: Baru diukur dari rilis sebelumnya, Perbaikan dari commit `fix(kernel…)`). Rilis dari run menampilkan teks yang sama untuk rilis yang terbit, **satu teks per rilis** (owner 26 Okt 2026, "mesti copas 2 kali"): KernelSU Next dan BakaSU dalam satu teks ID dan satu teks EN (`telegram.release_post`), perubahan yang sama ditulis sekali, yang khusus diberi nama root manager-nya. **Tanpa hash** (owner 31 Okt 2026, "tidak usah menggunakan angka/huruf acak"): perubahan root manager dan SUSFS ditulis dengan kata-kata biasa oleh `kernel/upstream.py` ("memperbaiki penyembunyian SELinux", "memperbaiki error saat compile"), dari judul commit hulu yang baru sejak rilis sebelumnya, juga setelah force-push. Kata kunci per bagian, bukan terjemahan bebas; judul yang tidak cocok hanya dihitung ("1 perubahan lain"); commit yang tidak masuk kernel ini dilewati (SUSFS LKM, kernel versi lain, Samsung, terjemahan, deps, CI, docs; untuk KernelSU Next hanya bagian kernel). Bagian yang sering muncul tapi belum dikenali ditambahkan ke `AREAS`. - **Workflow ditulis sederhana**, tampilannya terinspirasi WildKernel. Cara kerja GKI WildKernel tidak disalin, dan workflow repo lama tidak disalin begitu saja. - **Isi tiap bahasa harus sesuai bahasanya** (owner 26 Okt 2026): bagian English pada catatan rilis dan teks Telegram tidak boleh menyalin judul commit Indonesia. Commit `fix(kernel…)` baru menyertakan trailer `Release-Fix-EN: `; `Release-Fix-ID:` opsional untuk judul yang bukan Indonesia. Terjemahan empat commit lama ada di `kernel/telegram.py`. Catatan owner memakai kolom Indonesia dan English terpisah. Bila terjemahan commit belum tersedia, tampilkan rujukan sumber dengan keterangan English, jangan salin teks Indonesia atau mengarang terjemahan. - **Rilis lama** (r5, r6, r8, Cogan Smart v2.8.1/v2.8.2) disalin dari repo pendahulu sebagai arsip, dengan file dan SHA256 yang sama. Kernel arsip bernama **arsip r5, arsip r6, arsip r8** dengan tag berawalan `arsip-` (mis. `arsip-v26.10.22-r5`; owner 24 Okt 2026), supaya nomor rN repo ini tidak bentrok. Jangan memakai nama atau tag tanpa awalan itu untuk arsip. diff --git a/README.en.md b/README.en.md index 23a140b..b3c5aeb 100644 --- a/README.en.md +++ b/README.en.md @@ -93,7 +93,7 @@ Unicode fix (optional) passes the build + KMI gate on 157, 162, 172 and 176, and To publish an existing build, run **Rilis dari run** with the green Builder run ID. A run containing KernelSU Next and BakaSU becomes **one release** with both kernel ZIPs and their matching manager APKs, without rebuilding. Tick **diuji di HP** only if the owner tested every variant in that run on the phone. -Release notes include **What's New & Fixes**, compared with the previous release of the same root manager. The run's **Summary** provides **one Telegram post ready to copy** for the whole release (KernelSU Next and BakaSU in one text) in Indonesian and English, each in a code block with a copy button. Tick **teks_saja** to generate only notes and Telegram posts, including for an already published version; this mode does not publish or change a release. +Release notes include **What's New & Fixes**, compared with the previous release of the same root manager. The run's **Summary** provides **one Telegram post ready to copy** for the whole release (KernelSU Next and BakaSU in one text) in Indonesian and English, each in a code block with a copy button. Changes from the root manager and SUSFS developers are written in plain words (e.g. "fixes SELinux hiding"), without commit codes. Tick **teks_saja** to generate only notes and Telegram posts, including for an already published version; this mode does not publish or change a release. diff --git a/README.md b/README.md index 9417087..85d6d28 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Unicode fix (opsional) lolos build + gate KMI di 157, 162, 172 dan 176, dan terp Untuk menerbitkan build yang sudah selesai, jalankan **Rilis dari run** dengan ID run Builder yang hijau. Run yang berisi KernelSU Next dan BakaSU diterbitkan menjadi **satu rilis** dengan kedua ZIP kernel dan APK manager masing-masing, tanpa kompilasi ulang. Centang **diuji di HP** hanya bila semua varian dalam run itu sudah diuji owner. -Catatan rilis memuat **What's New & Fixes**, dibandingkan dengan rilis sebelumnya dari root manager yang sama. Di halaman **Summary** run, **satu Teks Telegram siap salin** tersedia untuk seluruh rilis (KernelSU Next dan BakaSU dalam satu teks) dalam Bahasa Indonesia dan English, masing-masing dalam kotak kode dengan tombol salin. Centang **teks_saja** bila hanya ingin membuat catatan dan teks Telegram, termasuk untuk versi yang sudah terbit; mode ini tidak menerbitkan atau mengubah rilis. +Catatan rilis memuat **What's New & Fixes**, dibandingkan dengan rilis sebelumnya dari root manager yang sama. Di halaman **Summary** run, **satu Teks Telegram siap salin** tersedia untuk seluruh rilis (KernelSU Next dan BakaSU dalam satu teks) dalam Bahasa Indonesia dan English, masing-masing dalam kotak kode dengan tombol salin. Perubahan dari pengembang root manager dan SUSFS ditulis dengan kata-kata biasa (mis. "memperbaiki penyembunyian SELinux"), tanpa kode commit. Centang **teks_saja** bila hanya ingin membuat catatan dan teks Telegram, termasuk untuk versi yang sudah terbit; mode ini tidak menerbitkan atau mengubah rilis. diff --git a/kernel/telegram.py b/kernel/telegram.py index e009999..46f0686 100644 --- a/kernel/telegram.py +++ b/kernel/telegram.py @@ -15,8 +15,17 @@ import re import subprocess import sys +import urllib.parse +import urllib.request from pathlib import Path +if __package__: # imported as kernel.telegram + from . import upstream as up +else: # run as kernel/telegram.py (Builder summary) + sys.path.insert(0, str(Path(__file__).resolve().parent)) + import upstream as up # noqa: E402 + +SUSFS_REPO = "https://gitlab.com/simonpunk/susfs4ksu" RELEASE_TAG = re.compile(r"^v\d{2}\.\d{2}\.\d{2}-r\d+$") # this repository's kernels (not arsip-, cogan-smart-) # Reviewed translations of historical subjects. New commits can supply Release-Fix-ID/EN trailers. FIX_EN = { @@ -33,24 +42,24 @@ "id": {"new": "✨ Baru", "fixes": "πŸ”§ Perbaikan", "note": "πŸ“ Catatan", "features": "🧩 Fitur", "same": "Komponen sama dengan {prev}", "first": "Rilis pertama varian ini", "baseline": "Dibandingkan dengan {prev}", "unknown": "Riwayat rilis belum dapat dibaca; perubahan belum dapat dibandingkan", - "no_fixes": "Tidak ada commit perbaikan kernel tambahan yang tercatat pada rentang ini", + "no_fixes": "Tidak ada perbaikan baru", "first_fixes": "Belum ada rilis pembanding untuk varian ini", "unknown_fixes": "Daftar commit perbaikan belum dapat dibaca", "untranslated_fix": "Perbaikan kernel tercatat dalam riwayat sumber", - "kernel": "Kernel ACK {old} β†’ {new}", "root": "{name} `{old}` β†’ `{new}`", "susfs": "SUSFS {old} β†’ {new}", - "ack": "Rilis ACK {old} β†’ {new}", "unicode": "Unicode fix WildKernel `{old}` β†’ `{new}`", + "kernel": "Kernel ACK {old} β†’ {new}", "susfs": "SUSFS {version} diperbarui", + "ack": "Rilis ACK {old} β†’ {new}", "unicode": "Unicode fix WildKernel diperbarui", "neutron": "Neutron clang {old} β†’ {new}", "nomount": "NoMount {old} β†’ {new}", "added": "Fitur baru: {names}", "removed": "Fitur dilepas: {names}", "tested": "βœ… Diuji di HP", "untested": "⚠️ Belum diuji di HP", "download": "⬇️ Unduh"}, "en": {"new": "✨ New", "fixes": "πŸ”§ Fixes", "note": "πŸ“ Note", "features": "🧩 Features", "same": "Same components as {prev}", "first": "First release of this variant", "baseline": "Compared with {prev}", "unknown": "Release history could not be read; changes could not be compared", - "no_fixes": "No additional kernel fix commits recorded in this range", + "no_fixes": "No new fixes", "first_fixes": "No earlier release of this variant to compare with", "unknown_fixes": "The fix commit list could not be read", "untranslated_fix": "Kernel fix recorded in the source history; English description not supplied", - "kernel": "ACK kernel {old} β†’ {new}", "root": "{name} `{old}` β†’ `{new}`", "susfs": "SUSFS {old} β†’ {new}", - "ack": "ACK release {old} β†’ {new}", "unicode": "WildKernel Unicode fix `{old}` β†’ `{new}`", + "kernel": "ACK kernel {old} β†’ {new}", "susfs": "SUSFS {version} updated", + "ack": "ACK release {old} β†’ {new}", "unicode": "WildKernel Unicode fix updated", "neutron": "Neutron clang {old} β†’ {new}", "nomount": "NoMount {old} β†’ {new}", "added": "New features: {names}", "removed": "Features dropped: {names}", "tested": "βœ… Tested on the phone", "untested": "⚠️ Not tested on the phone yet", "download": "⬇️ Download"}, @@ -120,7 +129,22 @@ def fixes(commits, lang="id"): return found -def whats_new(env, prev, lang, history_available=True): +def upstream_lines(env, prev, lang, upstream=None): + """[(What's new line, fix line or None)] of the root manager and SUSFS when their commit changed, in plain + words without hashes (owner 2026-10-31). upstream: {"root"/"susfs": new commit titles, None = unreadable}.""" + found, upstream = [], upstream or {} + if prev and prev.get("root") and not env.get("ROOT_COMMIT", "").startswith(prev["root"][1]): + # KernelSU Next: the kernel comes from the fork, its manager from KernelSU Next's own CI. + found.append(up.describe(env.get("ROOT_NAME", ""), upstream.get("root"), lang, + kernel_only=env.get("ROOT_NAME") == "KernelSU Next")) + if prev and prev.get("susfs") and not env.get("SUSFS_COMMIT", "").startswith(prev["susfs"][1]): + old, new = prev["susfs"][0], env.get("SUSFS_VERSION", "") + head = f"SUSFS {old} β†’ {new}" if old != new else WORDS[lang]["susfs"].format(version=new) + found.append(up.describe("SUSFS", upstream.get("susfs"), lang, head=head)) + return found + + +def whats_new(env, prev, lang, history_available=True, upstream=None): w = WORDS[lang] if not history_available: return [w["unknown"]] @@ -131,15 +155,11 @@ def whats_new(env, prev, lang, history_available=True): items.append(w["kernel"].format(old=prev["kernel"], new=env.get("LINUX_VERSION"))) if prev.get("ack") and env.get("ACK_TAG") and prev["ack"] != env["ACK_TAG"]: items.append(w["ack"].format(old=prev["ack"], new=env["ACK_TAG"])) - if prev.get("root") and not env.get("ROOT_COMMIT", "").startswith(prev["root"][1]): - items.append(w["root"].format(name=env.get("ROOT_NAME"), old=prev["root"][1][:12], new=env.get("ROOT_COMMIT", "")[:12])) - if prev.get("susfs") and not env.get("SUSFS_COMMIT", "").startswith(prev["susfs"][1]): - items.append(w["susfs"].format(old=f"{prev['susfs'][0]} `{prev['susfs'][1][:12]}`", - new=f"{env.get('SUSFS_VERSION')} `{env.get('SUSFS_COMMIT', '')[:12]}`")) + items += [line for line, _ in upstream_lines(env, prev, lang, upstream)] if prev.get("nomount") and prev["nomount"] != env.get("NOMOUNT"): items.append(w["nomount"].format(old=prev["nomount"], new=env.get("NOMOUNT"))) if prev.get("unicode") and env.get("UNICODE_FIX", "-") != "-" and not env["UNICODE_FIX"].startswith(prev["unicode"]): - items.append(w["unicode"].format(old=prev["unicode"], new=env["UNICODE_FIX"][:12])) + items.append(w["unicode"]) if prev.get("neutron") and env.get("NEUTRON") and prev["neutron"] != env["NEUTRON"]: items.append(w["neutron"].format(old=prev["neutron"], new=env["NEUTRON"])) old, new = feature_names(prev.get("features", "")), feature_names(env.get("FEATURES", "")) @@ -150,7 +170,15 @@ def whats_new(env, prev, lang, history_available=True): return items or [w["same"].format(prev=prev["tag"])] -def fix_items(lang, prev, fix_titles, history_available=True, fixes_available=True): +def fix_items(lang, prev, fix_titles, history_available=True, fixes_available=True, upstream_fixes=()): + """This repository's kernel fixes, then what the root manager and SUSFS developers fixed.""" + items = own_fixes(lang, prev, fix_titles, history_available, fixes_available) + if upstream_fixes and items == [WORDS[lang]["no_fixes"]]: + items = [] + return items + list(upstream_fixes) + + +def own_fixes(lang, prev, fix_titles, history_available=True, fixes_available=True): if isinstance(fix_titles, dict): if fix_titles.get(lang): return list(fix_titles[lang]) @@ -162,7 +190,12 @@ def fix_items(lang, prev, fix_titles, history_available=True, fixes_available=Tr return [WORDS[lang][key]] -def changelog_section(env, repo, prev=None, fix_titles=(), history_available=True, fixes_available=True, source_sha=""): +def upstream_fixes(env, prev, lang, upstream=None): + return [fix for _, fix in upstream_lines(env, prev, lang, upstream) if fix] + + +def changelog_section(env, repo, prev=None, fix_titles=(), history_available=True, fixes_available=True, source_sha="", + upstream=None): """The same measured changes as the Telegram post, included in the published release notes.""" lines = ["### What's New & Fixes", ""] for lang in ("id", "en"): @@ -171,23 +204,25 @@ def changelog_section(env, repo, prev=None, fix_titles=(), history_available=Tru if prev: link = f"[{prev['tag']}](https://github.com/{repo}/releases/tag/{prev['tag']})" lines += [w["baseline"].format(prev=link), ""] - lines += [f"**{w['new']}**", *[f"- {item}" for item in whats_new(env, prev, lang, history_available)], "", - f"**{w['fixes']}**", *[f"- {item}" for item in fix_items(lang, prev, fix_titles, history_available, fixes_available)], ""] + lines += [f"**{w['new']}**", *[f"- {item}" for item in whats_new(env, prev, lang, history_available, upstream)], "", + f"**{w['fixes']}**", *[f"- {item}" for item in fix_items(lang, prev, fix_titles, history_available, + fixes_available, upstream_fixes(env, prev, lang, upstream))], ""] if re.fullmatch(r"[0-9a-f]{40}", source_sha): lines += [f"", ""] return "\n".join(lines) def post(env, lang, prev=None, fix_titles=(), link="", tested=False, note="", history_available=True, - fixes_available=True, source_sha="", note_en=""): + fixes_available=True, source_sha="", note_en="", upstream=None): w = WORDS[lang] lines = ["🧠 Cogan Kernel Β· Xiaomi 14T Pro (rothko)", f"πŸ“¦ {env.get('RELEASE', '')} Β· ACK {env.get('LINUX_VERSION', '')} Β· {env.get('ROOT_NAME', '')} + SUSFS " f"{env.get('SUSFS_VERSION', '')}"] if prev: lines += ["", "πŸ“Œ " + w["baseline"].format(prev=prev["tag"])] - lines += ["", w["new"], *[f"β€’ {item}" for item in whats_new(env, prev, lang, history_available)], - "", w["fixes"], *[f"β€’ {title}" for title in fix_items(lang, prev, fix_titles, history_available, fixes_available)]] + lines += ["", w["new"], *[f"β€’ {item}" for item in whats_new(env, prev, lang, history_available, upstream)], + "", w["fixes"], *[f"β€’ {title}" for title in fix_items(lang, prev, fix_titles, history_available, fixes_available, + upstream_fixes(env, prev, lang, upstream))]] localized_note = note if lang == "id" else note_en if localized_note.strip(): lines += ["", w["note"], *localized_note.replace("\\n", "\n").strip().splitlines()] @@ -227,11 +262,13 @@ def release_post(builds, lang, link="", tested=False, note="", note_en=""): if prevs: same = len(prevs) == len(builds) and len({tag for _, tag in prevs}) == 1 lines += ["", "πŸ“Œ " + w["baseline"].format(prev=prevs[0][1] if same else ", ".join(f"{tag} ({label})" for label, tag in prevs))] - new = merged([(label, whats_new(env, measured.get("prev"), lang, measured.get("history_available", True))) + new = merged([(label, whats_new(env, measured.get("prev"), lang, measured.get("history_available", True), + measured.get("upstream"))) for label, (env, measured) in zip(labels, builds)]) fixed = merged([(label, fix_items(lang, measured.get("prev"), measured.get("fix_titles", ()), - measured.get("history_available", True), measured.get("fixes_available", True))) - for label, (_, measured) in zip(labels, builds)]) + measured.get("history_available", True), measured.get("fixes_available", True), + upstream_fixes(env, measured.get("prev"), lang, measured.get("upstream")))) + for label, (env, measured) in zip(labels, builds)]) lines += ["", w["new"], *[f"β€’ {item}" for item in new], "", w["fixes"], *[f"β€’ {item}" for item in fixed]] localized_note = note if lang == "id" else note_en if localized_note.strip(): @@ -266,6 +303,12 @@ def summary_section(posts): def gh(path): + if path.startswith("gitlab:"): # SUSFS lives on GitLab; its API needs no token for a public project + try: + with urllib.request.urlopen("https://gitlab.com/api/v4/" + path[len("gitlab:"):], timeout=60) as reply: + return json.load(reply) + except (OSError, ValueError): + return None try: result = subprocess.run(["gh", "api", path], capture_output=True, text=True, timeout=120) return json.loads(result.stdout) if result.returncode == 0 and result.stdout.strip() else None @@ -273,6 +316,32 @@ def gh(path): return None +def new_titles(api, repo_url, old, new): + """Titles of the commits new has and old has not, also after a force-push (the KernelSU Next fork + rebases), or None when the comparison cannot be read.""" + github = re.fullmatch(r"https://github\.com/([^/]+/[^/]+?)(?:\.git)?/?", repo_url or "") + gitlab = re.fullmatch(r"https://gitlab\.com/(.+?)(?:\.git)?/?", repo_url or "") + + def titles(base, head): + if github: + data = api(f"repos/{github[1]}/compare/{base}...{head}") + commits = data.get("commits") if isinstance(data, dict) else None + if isinstance(commits, list): + return [(c.get("commit") or {}).get("message", "").split("\n")[0] for c in commits] + elif gitlab: + data = api(f"gitlab:projects/{urllib.parse.quote(gitlab[1], safe='')}/repository/compare?from={base}&to={head}") + commits = data.get("commits") if isinstance(data, dict) else None + if isinstance(commits, list): + return [c.get("title", "") for c in commits] + return None + + ahead = titles(old, new) + behind = titles(new, old) if ahead else [] + if ahead is None or behind is None: + return None + return [title for title in ahead if title not in set(behind)] + + def changes(env, repo, sha="", api=gh): """Read the comparison once; release notes and both copyable posts use the same result.""" releases = api(f"repos/{repo}/releases?per_page=50") @@ -284,8 +353,13 @@ def changes(env, repo, sha="", api=gh): if isinstance(comparison, dict) and isinstance(comparison.get("commits"), list) and comparison.get("status") != "behind": titles = {lang: fixes(comparison["commits"], lang) for lang in ("id", "en")} compared = True + upstream = {} + if prev and prev.get("root") and not env.get("ROOT_COMMIT", "").startswith(prev["root"][1]): + upstream["root"] = new_titles(api, env.get("ROOT_REPO", ""), prev["root"][1], env.get("ROOT_COMMIT", "")) + if prev and prev.get("susfs") and not env.get("SUSFS_COMMIT", "").startswith(prev["susfs"][1]): + upstream["susfs"] = new_titles(api, SUSFS_REPO, prev["susfs"][1], env.get("SUSFS_COMMIT", "")) return {"prev": prev, "fix_titles": titles, "history_available": available, - "fixes_available": compared or (available and prev is None), "source_sha": sha} + "fixes_available": compared or (available and prev is None), "source_sha": sha, "upstream": upstream} def main(argv=None, api=gh): diff --git a/kernel/upstream.py b/kernel/upstream.py new file mode 100644 index 0000000..1066ca8 --- /dev/null +++ b/kernel/upstream.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Upstream changes in plain words for the release text (owner 2026-10-31: no hashes, end users do not +read them; "memperbaiki selinux", "memperbaiki driver loop"). + +The commits of a root manager or SUSFS between two releases become short Indonesian and English phrases +by keyword: the area a commit touches (SELinux hiding, mount hiding, compile, ...) and whether it fixes or +improves it. Nothing is translated freely. A title that matches no area is only counted; a title that does +not reach this kernel is left out: SUSFS for the LKM variant, other kernel versions, Samsung, translations, +dependency bumps, CI and docs, and for KernelSU Next everything outside its kernel part (its manager comes +from KernelSU Next's own CI, not from the fork the kernel uses). +""" +import re + +LIMIT = 3 # phrases per line; the rest are counted +CLEAN = re.compile(r"\s*\((?:issue )?https?://[^)]*\)|\s*\(#\d+\)|\s+$") +SKIP = re.compile(r"\(LKM\)|weblate|translat|\bdeps?\b|renovate|\bbump|\bcargo\b|\bcrates?\b|^ci\b|workflow|" + r"readme|\bdocs?\b|^merge\b|typo|clang-format|\bformat|useless|unused|clean ?up|refactor|rename|" + r"samsung|\bkdp\b|\brkp\b|defex|\b(?:4\.\d+|5\.\d+|6\.(?:[2-9]|1\d))\b", re.I) +FIX = re.compile(r"\bfix|\bbugs?\b|crash|leak|\brace\b|\buaf\b|use.after.free|revert|correct|prevent|avoid|" + r"\bdo not\b|\bdon't\b|wrong|broken|\bbalance|\bnull\b|panic|deadlock|\berror", re.I) +# (keywords, Indonesian (improves, fixes ...), English (improves, fixes ...)); the first match wins. +AREAS = [ + (r"selinux", ("meningkatkan penyembunyian SELinux", "penyembunyian SELinux"), + ("improves SELinux hiding", "SELinux hiding")), + (r"smaps|sus_map|\bmaps\b", ("meningkatkan anti deteksi lewat peta memori", "anti deteksi lewat peta memori"), + ("improves anti-detection via memory maps", "anti-detection via memory maps")), + (r"mount", ("meningkatkan penyembunyian mount", "penyembunyian mount"), ("improves mount hiding", "mount hiding")), + (r"sus_path|dirent|readlink|hidden (?:file|name|path)", ("meningkatkan penyembunyian file dan folder", + "penyembunyian file dan folder"), ("improves file and folder hiding", "file and folder hiding")), + (r"kstat|statfs", ("meningkatkan penyamaran info file", "penyamaran info file"), + ("improves file info spoofing", "file info spoofing")), + (r"uname|cmdline|spoof", ("meningkatkan penyamaran info sistem", "penyamaran info sistem"), + ("improves system info spoofing", "system info spoofing")), + (r"side.?channel|timing|detect|zygote|isolated", ("meningkatkan anti deteksi", "anti deteksi"), + ("improves anti-detection", "anti-detection")), + (r"rebase|android14|\bgki\b|patch offset", ("menyesuaikan dengan kernel ACK terbaru", "kecocokan dengan kernel ACK"), + ("adapts to the newest ACK kernel", "ACK kernel compatibility")), + (r"crash|\buaf\b|use.after.free|panic|deadlock|null pointer|oops|\brace\b|leak", + ("meningkatkan stabilitas", "potensi crash"), ("improves stability", "possible crashes")), + (r"compil|\bbuild\b|kbuild|toolchain|clang", ("memperbarui proses compile", "error saat compile"), + ("updates the build", "a compile error")), + (r"\blogs?\b|logging|noisy|spam", ("mengurangi log yang tidak perlu", "log kernel"), + ("reduces unnecessary logs", "kernel logging")), + (r"perf\b|performance|optimi|fast ?path|faster|speed|latency|offload|kthread", + ("meningkatkan performa", "performa"), ("improves performance", "performance")), + (r"\bloop\b", ("memperbarui driver loop", "driver loop"), ("updates the loop driver", "the loop driver")), + (r"module|preinit|hot.?install", ("meningkatkan dukungan modul", "dukungan modul"), + ("improves module support", "module support")), + (r"manager|webui|\bui\b|monet|theme", ("memperbarui aplikasi manager", "aplikasi manager"), + ("updates the manager app", "the manager app")), + (r"ksud|ksuinit|userspace", ("memperbarui alat root (ksud)", "alat root (ksud)"), + ("updates the root tool (ksud)", "the root tool (ksud)")), + (r"sync with|upstream", ("menyelaraskan dengan KernelSU resmi", "keselarasan dengan KernelSU resmi"), + ("syncs with the official KernelSU", "sync with the official KernelSU")), + (r"\bsu\b|allow.?list|root access|\bshell\b", ("memperbarui pengaturan akses root", "akses root"), + ("updates root access control", "root access")), + (r"kprobe|syscall|supercall|\bhooks?\b", ("memperbarui cara root terpasang di kernel", "cara root terpasang di kernel"), + ("updates how root hooks into the kernel", "the kernel hooks")), +] +WORDS = {"id": {"and": "dan", "fix": "memperbaiki", "other": "{n} perubahan lain", "technical": "{n} perubahan teknis", + "updated": "{name} diperbarui", "unknown": "{name} diperbarui ke versi terbaru dari pengembang", + "minor": "{name} diperbarui (perubahan kecil)"}, + "en": {"and": "and", "fix": "fixes", "other": "{n} other change{s}", "technical": "{n} technical change{s}", + "updated": "{name} updated", "unknown": "{name} updated to the developer's newest version", + "minor": "{name} updated (minor changes)"}} + + +def relevant(titles, kernel_only=False): + """Titles that reach this kernel, without links and PR numbers, each once.""" + kept = [] + for title in titles: + title = CLEAN.sub("", title.splitlines()[0] if title else "") + if not title or SKIP.search(title) or (kernel_only and not re.match(r"(?:fix\()?kernel", title, re.I)): + continue + if title not in kept: + kept.append(title) + return kept + + +def classify(titles, lang): + """(improvement phrases, fixed things, how many titles match no area): each phrase once, in the order + of AREAS (the parts end users notice first), whatever order the commits came in.""" + side = 0 if lang == "id" else 1 + improved, fixed, other = set(), set(), 0 + for title in titles: + index = next((i for i, area in enumerate(AREAS) if re.search(area[0], title, re.I)), None) + if index is None: + other += 1 + else: + (fixed if FIX.search(title) else improved).add(index) + return ([AREAS[i][1 + side][0] for i in sorted(improved)], [AREAS[i][1 + side][1] for i in sorted(fixed)], other) + + +def listing(parts, lang): + if len(parts) < 2: + return "".join(parts) + return ", ".join(parts[:-1]) + f" {WORDS[lang]['and']} " + parts[-1] + + +def describe(name, titles, lang, head="", kernel_only=False): + """(What's new line, fix line or None) of one updated component. titles None = not readable. + head replaces " updated" when the version itself changed, e.g. "SUSFS v2.2.0 β†’ v2.3.0".""" + w = WORDS[lang] + if titles is None: + return head or w["unknown"].format(name=name), None + kept = relevant(titles, kernel_only) + if not kept: + return head or w["minor"].format(name=name), None + improved, fixed, other = classify(kept, lang) + other += max(0, len(improved) - LIMIT) + max(0, len(fixed) - LIMIT) + improved, fixed = improved[:LIMIT], fixed[:LIMIT] + line = head or w["updated"].format(name=name) + if improved: + line += ": " + listing(improved + ([w["other"].format(n=other, s="s" if other > 1 else "")] if other else []), lang) + elif other: + line += f" ({w['technical'].format(n=other, s='s' if other > 1 else '')})" + return line, f"{name}: {w['fix']} {listing(fixed, lang)}" if fixed else None diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index 691d214..46964ee 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -38,7 +38,7 @@ def setUp(self): (build / "build.env").write_text("ROOT_NAME=KernelSU Next\nLINUX_VERSION=6.1.176\n") (self.root / "files/1-KERNEL_fixture.zip").write_text("kernel fixture") (self.root / "notes.md").write_text("notes fixture\n") - for name in ("release_notes.py", "release_from_run.py", "telegram.py"): + for name in ("release_notes.py", "release_from_run.py", "telegram.py", "upstream.py"): shutil.copyfile(WORKFLOW.parents[2] / "kernel" / name, self.root / "kernel" / name) (self.root / "releases.json").write_text("[]") (self.root / "compare.json").write_text('{"commits": []}') @@ -188,8 +188,8 @@ def test_whats_new_and_fixes_are_in_the_release_and_one_copyable_post_per_langua self.assertIn("BakaSU & KernelSU Next + SUSFS v2.3.0", post) self.assertEqual(post.count("perbaikan ZIP ARM64" if lang == "id" else "Fix the ARM64 ZIP package"), 1) self.assertEqual(post.count("android14-6.1-2026-09_r4 β†’ android14-6.1-2026-09_r5"), 1) - self.assertIn("KernelSU Next `cccccccccccc` β†’ `aaaaaaaaaaaa`", post) - self.assertIn("BakaSU `cccccccccccc` β†’ `bbbbbbbbbbbb`", post) + self.assertNotIn("`cccccccccccc`", post) # no hashes for end users (owner 2026-10-31) + self.assertIn("BakaSU" if lang == "en" else "BakaSU diperbarui", post) if lang == "en": self.assertNotIn("perbaikan ZIP ARM64", post) self.assertIn("Tested on my device.", post) diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 27ab648..a717fb0 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -38,7 +38,7 @@ def test_whats_new_is_measured_against_the_previous_release(self): self.assertEqual(tg.whats_new(R2, prev, "en"), ["New features: Unicode fix"]) newer = {**R2, "LINUX_VERSION": "6.1.180", "SUSFS_COMMIT": "1" * 40, "NOMOUNT": "v2.3.0"} self.assertEqual(tg.whats_new(newer, prev, "id")[:3], ["Kernel ACK 6.1.176 β†’ 6.1.180", - "SUSFS v2.3.0 `c27ebe93c68e` β†’ v2.3.0 `111111111111`", + "SUSFS v2.3.0 diperbarui", "NoMount v2.2.0 β†’ v2.3.0"]) self.assertEqual(tg.whats_new(R1, prev, "en"), ["Same components as v26.10.23-r1"]) self.assertEqual(tg.whats_new(R1, None, "id"), ["Rilis pertama varian ini"]) @@ -78,7 +78,7 @@ def test_post_in_both_languages_with_status_and_link(self): english = tg.post(R2, "en", prev, link="https://x/run") self.assertIn("✨ New\nβ€’ New features: Unicode fix", english) self.assertIn("⚠️ Not tested on the phone yet", english) - self.assertIn("πŸ”§ Fixes\nβ€’ No additional kernel fix commits recorded in this range", english) + self.assertIn("πŸ”§ Fixes\nβ€’ No new fixes", english) def test_cli_writes_both_posts_and_survives_without_the_api(self): with tempfile.TemporaryDirectory() as temp: @@ -108,7 +108,7 @@ def test_ack_revision_and_unicode_patch_updates_are_visible(self): current = {**R2, "ACK_TAG": "android14-6.1-2026-09_r5", "UNICODE_FIX": "f" * 40, "NEUTRON": "24102026"} text = "\n".join(tg.whats_new(current, previous, "id")) self.assertIn("Rilis ACK android14-6.1-2026-09_r4 β†’ android14-6.1-2026-09_r5", text) - self.assertIn("Unicode fix WildKernel `fa76c90e2cb2` β†’ `ffffffffffff`", text) + self.assertIn("Unicode fix WildKernel diperbarui", text) self.assertIn("Neutron clang 06092026 β†’ 24102026", text) def test_old_preview_ignores_its_own_and_newer_releases(self): @@ -135,7 +135,7 @@ def test_failed_compare_does_not_claim_there_were_no_fixes(self): measured = tg.changes(R2, "r/r", "f" * 40, lambda path: RELEASES if "/releases?" in path else None) text = tg.post(R2, "en", **measured) self.assertIn("The fix commit list could not be read", text) - self.assertNotIn("No additional kernel fix commits", text) + self.assertNotIn("No new fixes", text) def test_fix_comparison_uses_the_recorded_build_commit_instead_of_the_tag_target(self): body = rn.notes(R1, [], "https://run", "r/r") + f"\n\n" @@ -200,7 +200,7 @@ def test_owner_notes_are_kept_in_their_own_language(self): def test_empty_localized_fix_lists_still_report_no_recorded_fixes(self): english = tg.post(R2, "en", prev=tg.previous(RELEASES, R2), fix_titles={"id": [], "en": []}) - self.assertIn("No additional kernel fix commits recorded", english) + self.assertIn("No new fixes", english) self.assertNotIn("English description not supplied", english) def test_one_build_release_post_is_the_build_post(self): @@ -219,8 +219,8 @@ def test_both_root_managers_share_one_post(self): text = tg.release_post(builds, "id", link="https://x/r4", tested=True, note="catatan", note_en="note") self.assertIn("πŸ“¦ v26.10.24-r2 Β· ACK 6.1.176 Β· BakaSU & KernelSU Next + SUSFS v2.3.0", text) self.assertIn("πŸ“Œ Dibandingkan dengan v26.10.24-r3 (BakaSU), v26.10.23-r1 (KernelSU Next)", text) - self.assertIn("✨ Baru\nβ€’ Fitur baru: Unicode fix\nβ€’ BakaSU `2ea131d9d44e` β†’ `d5e9e025b8b4`\n", text) # shared once - self.assertIn("πŸ”§ Perbaikan\nβ€’ BakaSU: Tidak ada commit perbaikan kernel tambahan yang tercatat pada rentang ini\n" + self.assertIn("✨ Baru\nβ€’ Fitur baru: Unicode fix\nβ€’ BakaSU diperbarui ke versi terbaru dari pengembang\n", text) # shared once + self.assertIn("πŸ”§ Perbaikan\nβ€’ BakaSU: Tidak ada perbaikan baru\n" "β€’ KernelSU Next: SUSFS di ACK lama\n", text) self.assertIn("πŸ“ Catatan\ncatatan\n\n🧩 Fitur: SUSFS, BBRv3", text) self.assertEqual((text.count("🧠"), text.count("⬇️ Unduh: https://x/r4"), text.count("βœ… Diuji di HP")), (1, 1, 1)) @@ -238,6 +238,43 @@ def test_combined_post_names_what_differs(self): self.assertIn("β€’ BakaSU 6.1.172: First release of this variant", text) self.assertIn("🧩 Features (BakaSU 6.1.172): SUSFS, Full LTO\n🧩 Features (KernelSU Next 6.1.176): SUSFS, BBRv3", text) + def test_upstream_changes_are_plain_words_without_hashes(self): + # Owner 2026-10-31 (r7 text): "tidak usah menggunakan angka/huruf acak", e.g. "memperbaiki selinux". + bsu = {**R2, "ROOT_NAME": "BakaSU", "ROOT_REPO": "https://github.com/Baka-SU/BakaSU", + "ROOT_COMMIT": "5785bdb80913" + "0" * 28, "SUSFS_COMMIT": "ee155a1ad421" + "0" * 28} + prev = {"tag": "v26.10.29-r6", "sha": "", "root": ("BakaSU main", "d5e9e025b8b4"), "susfs": ("v2.3.0", "8b2599024117")} + asked = [] + + def api(path): + asked.append(path) + if path.startswith("repos/Baka-SU/BakaSU/compare/d5e9e025b8b4...5785bdb80913"): + return {"commits": [{"commit": {"message": "ksud, ksuinit: block vklp_smaps_rollup"}}]} + if path.startswith("gitlab:projects/simonpunk%2Fsusfs4ksu/repository/compare?from=8b2599024117&to=ee155a1ad421"): + return {"commits": [{"title": "KernelSU: Fix compiler error"}]} + return [{"tag_name": "v26.10.29-r6", "body": ""}] if "/releases?" in path else {"commits": []} + upstream = {"root": tg.new_titles(api, bsu["ROOT_REPO"], "d5e9e025b8b4", bsu["ROOT_COMMIT"]), + "susfs": tg.new_titles(api, tg.SUSFS_REPO, "8b2599024117", bsu["SUSFS_COMMIT"])} + self.assertIn(f"repos/Baka-SU/BakaSU/compare/{bsu['ROOT_COMMIT']}...d5e9e025b8b4", asked) # force-push check + text = tg.post(bsu, "id", prev=prev, fix_titles={"id": [], "en": []}, upstream=upstream) + self.assertIn("✨ Baru\nβ€’ BakaSU diperbarui: meningkatkan anti deteksi lewat peta memori\nβ€’ SUSFS v2.3.0 diperbarui\n", text) + self.assertIn("πŸ”§ Perbaikan\nβ€’ SUSFS: memperbaiki error saat compile\n", text) + self.assertNotIn("Tidak ada perbaikan baru", text) + english = tg.post(bsu, "en", prev=prev, fix_titles={"id": [], "en": []}, upstream=upstream) + self.assertIn("β€’ BakaSU updated: improves anti-detection via memory maps", english) + self.assertIn("πŸ”§ Fixes\nβ€’ SUSFS: fixes a compile error\n", english) + for post in (text, english): + self.assertNotRegex(post, r"[0-9a-f]{7,}") + + def test_a_rebased_fork_only_reports_titles_it_did_not_have(self): + def api(path): + if path.endswith("/compare/old...new"): + return {"commits": [{"commit": {"message": t}} for t in ("kernel: selinux_hide: Even out attr/current timing", + "kernel: susfs: Optimize path flagging")]} + return {"commits": [{"commit": {"message": "kernel: susfs: Optimize path flagging"}}]} + self.assertEqual(tg.new_titles(api, "https://github.com/pershoot/KernelSU-Next", "old", "new"), + ["kernel: selinux_hide: Even out attr/current timing"]) + self.assertIsNone(tg.new_titles(lambda path: None, "https://github.com/pershoot/KernelSU-Next", "old", "new")) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_upstream.py b/tests/test_upstream.py new file mode 100644 index 0000000..83fe0f4 --- /dev/null +++ b/tests/test_upstream.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Offline tests for kernel/upstream.py: upstream commits in plain words, never hashes (owner 2026-10-31).""" +import unittest + +from kernel import upstream as up + +# The real ranges of r7 (v26.10.29-r6 β†’ v26.10.31-r7), as new titles after a force-push of the fork. +KSUN_R7 = [ + "fix(kernel): align SELinux hide policy parsing (https://github.com/tiann/KernelSU/pull/3936)", + "kernel: Balance file references when reopening wrapped files (https://github.com/tiann/KernelSU/pull/3958)", + "kernel: Remove noisy logs from critical and frequent paths (https://github.com/tiann/KernelSU/pull/3960)", + "kernel: remove useless code (https://github.com/tiann/KernelSU/pull/3945)", + "kernel: selinux_hide: Even out attr/current timing", + "kernel: susfs (LKM): sus_mount: cover the mounts a magic-mount store marks", + "kernel: susfs (LKM): or_entry: do not putname twice on 5.10", + "userspace: Update waitsys to fix crash", +] + + +class UpstreamTest(unittest.TestCase): + def test_r7_in_plain_indonesian_and_english(self): + self.assertEqual(up.describe("SUSFS", ["KernelSU: Fix compiler error"], "id", head="SUSFS v2.3.0 diperbarui"), + ("SUSFS v2.3.0 diperbarui", "SUSFS: memperbaiki error saat compile")) + self.assertEqual(up.describe("BakaSU", ["ksud, ksuinit: block vklp_smaps_rollup"], "id"), + ("BakaSU diperbarui: meningkatkan anti deteksi lewat peta memori", None)) + self.assertEqual(up.describe("KernelSU Next", KSUN_R7, "id", kernel_only=True), + ("KernelSU Next diperbarui: meningkatkan penyembunyian SELinux, mengurangi log yang tidak perlu " + "dan 1 perubahan lain", "KernelSU Next: memperbaiki penyembunyian SELinux")) + self.assertEqual(up.describe("KernelSU Next", KSUN_R7, "en", kernel_only=True), + ("KernelSU Next updated: improves SELinux hiding, reduces unnecessary logs and 1 other change", + "KernelSU Next: fixes SELinux hiding")) + + def test_what_never_reaches_this_kernel_is_left_out(self): + self.assertEqual(up.relevant(KSUN_R7, kernel_only=True), [ + "fix(kernel): align SELinux hide policy parsing", + "kernel: Balance file references when reopening wrapped files", + "kernel: Remove noisy logs from critical and frequent paths", + "kernel: selinux_hide: Even out attr/current timing"]) + noise = ["manager: update translations from Weblate (#636)", "deps: Update Rust crate libc to v0.2.191 (#627)", + "Update README.md", "kernel: compat: samsung_kdp: Fix build on kernels >= 6.12"] + self.assertEqual(up.relevant(noise), []) + self.assertEqual(up.relevant(["kernel: Rebase susfs patches onto android14-6.1.176"]), + ["kernel: Rebase susfs patches onto android14-6.1.176"]) + + def test_unreadable_or_minor_ranges_still_say_what_was_updated(self): + self.assertEqual(up.describe("BakaSU", None, "id"), ("BakaSU diperbarui ke versi terbaru dari pengembang", None)) + self.assertEqual(up.describe("BakaSU", ["Update README.md"], "en"), ("BakaSU updated (minor changes)", None)) + self.assertEqual(up.describe("BakaSU", ["kernel: limit code generators on CONFIG_KSU (#639)", + "kernel: something else"], "en"), ("BakaSU updated (2 technical changes)", None)) + self.assertEqual(up.describe("SUSFS", None, "en", head="SUSFS v2.2.0 β†’ v2.3.0"), ("SUSFS v2.2.0 β†’ v2.3.0", None)) + + def test_long_ranges_stay_short(self): + titles = ["kernel: Fix another selinux leak", "kernel: SUS_MOUNT: Fix race condition in clone_mnt()", + "kernel: SUS_KSTAT: fix f_flags left zeroed", "kernel: Fix compiling issues", "susfs: Add uname spoof", + "kernel: optimize fast path", "KernelSU: Sync with the official KernelSU main repo", + "manager: new theme", "kernel: add module preinit"] + line, fixes = up.describe("SUSFS", titles, "id") + self.assertEqual(line, "SUSFS diperbarui: meningkatkan penyamaran info sistem, meningkatkan performa, " + "meningkatkan dukungan modul dan 3 perubahan lain") + self.assertEqual(fixes, "SUSFS: memperbaiki penyembunyian SELinux, penyembunyian mount dan penyamaran info file") + + +if __name__ == "__main__": + unittest.main()