Six small review-list items rolled together — all CI/dev-tooling, no
runtime behaviour change.
#12 Dockerfile: pin Rust 1.95.0 and cargo-ndk 4.1.2 (was floating
`stable` + latest cargo-ndk on monthly rebuild). Versions live
in ENV vars to make the next bump a one-line edit.
#13 Add shellcheck to lint job. SC2034/SC3043 excluded — Magisk
reads SKIPUNZIP externally; Android's /system/bin/sh (mksh on
Pixel) does support `local` despite POSIX. Verified locally
that the 11 .sh files (module-side + dev tooling) pass.
shellcheck baked into the CI image via apt; inline apt-get
fallback covers the window before image rebuild.
#24 ci.yml keystore.properties: replace heredoc with `printf '%s\n'`.
Heredoc without single-quoted EOF re-expands $, backticks and
backslashes in the password — printf takes the value verbatim.
#31 scripts/release.py::patch_file now hard-fails when a regex
pattern doesn't match (was silently leaving stale versions).
#32 Split rotate_fragments_into_history into rotate + delete steps
so release.py can save_json + write_md *before* unlinking the
fragment files. If anything in between fails, fragments are
still on disk and the run is retryable.
#37 codegen-interfaces.py: emit `assert!(matches_vpn(…), msg)` /
`assert!(!matches_vpn(…), msg)` instead of
`assert_eq!(matches_vpn(…), true/false, msg)` —
clippy::bool_assert_comparison was firing on every generated
row under `cargo clippy --tests`. Both generated test modules
regenerated. CI's clippy steps now also pass `--tests` so this
class of regression is caught.
Repo had ~1800 lines of Python (kmod/build.py, scripts/*, zygisk/build.py,
portshide/build-zip.py) with no formatter or linter. Long-lived scripts
like scripts/release.py and scripts/codegen-interfaces.py benefit from
catching unused-import / undefined-name / outdated-syntax issues early.
pyproject.toml — ruff config, target-py312, line-length 100,
rules E F W I B UP SIM. Excludes zygisk/third_party,
target/, .claude/.
ci.yml — astral-sh/ruff-action@v4 for `format --check` and `check`,
ahead of the slow Rust/Gradle steps so it fails fast.
docs/development.md — add `uvx ruff …` to the local-lint snippet.
Cleanup applied (`ruff format` + `ruff check --fix`):
- reformat: kmod/build.py, scripts/{changelog_lib,codegen-interfaces,
release,stats}.py, zygisk/build.py
- I001: split multi-name imports onto separate lines after the
sys.path.insert prelude (kmod/build.py, zygisk/build.py)
- E501 manual: wrap one console.print line in scripts/release.py
Stdlib-only invariant from scripts/build_lib.py is preserved — ruff is
a dev/CI tool, not imported at runtime.
Two changes that together eliminate changelog merge conflicts from
concurrent PRs:
1. **CHANGELOG.md is regenerated only by release.py.** The previous cut
still had every changelog.py invocation rewrite CHANGELOG.md with a
different [Unreleased] block, so two PRs producing different
unreleased content collided on the MD file. Checked-in CHANGELOG.md
now contains released versions only. Unreleased is rendered on
demand from changelog.d/ via scripts/preview-changelog.py — prints
to stdout, writes nothing.
2. **Fragment format: Markdown instead of TOML.** Filenames now look
like `<type>-<slug>-<hex4>.md` (e.g.
`fixed-dev-version-mismatch-a1b2.md`). Type is readable at-a-glance
in the directory listing; 4-char random hex prevents collision when
two PRs pick the same slug. Body is plain Markdown with `## English`
/ `## Русский` sections — renders directly on GitHub, no YAML/TOML
parser dependency.
- scripts/changelog_lib.py: MD parser replaces tomllib. render_full_md
drops the [Unreleased] block; write_md(data) signature simplified;
render_unreleased_md(fragments) for on-demand preview.
- scripts/changelog.py: writes <type>-<slug>-<hex4>.md, no MD regen.
- scripts/release.py: updated to the new write_md signature.
- scripts/preview-changelog.py: new.
- changelog.d/*.md: 10 existing TOML fragments migrated to MD. One
fragment (changelog-entries-now-live-as-per) updated to say Markdown
instead of TOML since that's the final state by the time this ships.
- CHANGELOG.md: regenerated — Unreleased block gone.
- .gitattributes: merge=union moved from *.toml to *.md.
- docs/changelog.md, docs/releasing.md, CONTRIBUTING.md,
changelog.d/README.md, CLAUDE.md: describe the new format + flow.
The JSON `unreleased` section was a conflict magnet — every PR touching
it collided with every other PR touching it. Fragments sidestep that:
each entry is its own TOML file, two PRs get two files, merges don't
touch the same bytes.
- changelog_lib.py: add FRAGMENTS_DIR, load_fragments(),
fragments_as_sections(), rotate_fragments_into_history();
render_full_md() now takes fragments explicitly.
- changelog.py: write a TOML fragment to changelog.d/<ts>-<slug>.toml
instead of appending to JSON. Auto-derive slug from EN text; accept
--slug override.
- release.py: load fragments, rotate into history[0], delete them.
- lsposed/app/src/main/assets/changelog.json: drop `unreleased` key;
only `history` remains (the 8 unreleased entries migrated to
changelog.d/*.toml). CHANGELOG.md regenerates byte-identical.
- UpdateChecker.kt: fix long-standing loadChangelog bug — was calling
parseChangelogEntry on the root object, which has no `version` field,
so the parse always threw and the function silently returned null.
Simplify ChangelogData to {history} and parse history[]; drop the
unused `current` field that DashboardScreen just concatenated back in.
- .gitattributes: mark changelog.d/*.toml as merge=union, belt-and-
suspenders fallback for same-filename collisions (rare — filenames
carry a timestamp).
- docs/changelog.md, docs/releasing.md, CONTRIBUTING.md,
changelog.d/README.md: describe the new flow.
Rework the changelog and release flow to remove the aspirational
top-level version that made it unclear whether new entries were
landing in an already-released section.
Schema change: `changelog.json` now has an explicit `unreleased`
object instead of hoisting the upcoming version to the top level. The
old `{version, sections, history}` layout becomes
`{unreleased, history}`, with the previously-released version moved
into `history[0]`.
New entries always go into `unreleased` via `changelog.py`. Releasing
is a single atomic operation (`release.py X.Y.Z`) that promotes
`unreleased` into `history[0]` with the target version number,
propagates the version to every source file, and regenerates the
markdown artifacts.
Script renames:
- `_changelog.py` → `changelog_lib.py` (no more underscore-prefixed
module that's imported by two siblings)
- `changelog-add.py` → `changelog.py`
- `update-version.py` → `release.py` (does more than just version
propagation — the name now reflects the full release action)
CHANGELOG.md rendering follows Keep a Changelog: a `## [Unreleased]`
block appears on top only when there are unreleased entries; the
update-json/changelog.md shown in Magisk/KSU popups still skips
Unreleased (only released versions make sense there).
Docs (docs/changelog.md, docs/releasing.md, CONTRIBUTING.md, CLAUDE.md)
updated with the new commands and the clarified model.
CLAUDE.md additionally gains a "read these before doing any work"
section that lists the contributor docs — so future sessions load the
workflow rules into context instead of skipping them as optional.