Thanks for your interest in improving janitor. This document describes the
development workflow, coding conventions, and how to add new subcommands.
Requirements:
- Rust 1.85+ (install via rustup).
- A Linux host or VM (the code is Linux-only; uses
lchown,/etc/passwd,setfacl). dockerfor the end-to-end test harness.aclpackage installed for local ACL testing:sudo apt install acl.
git clone https://github.com/Tristram1337/janitor
cd janitor
cargo build
cargo test# Unit tests.
cargo test --release
# End-to-end Docker smoke tests.
cargo build --release
docker build -f tests/Dockerfile -t janitor-test .
docker run --rm janitor-testCI runs the same commands on every push and pull request.
cargo fmtmust be clean.rustfmt.tomlpins the style.cargo clippy -- -D warningsmust be clean.- Every new subcommand MUST:
- Take an auto-snapshot before mutation.
- Honor the global
--dry-runflag (print shell-equivalent actions). - Refuse to follow symlinks for ownership changes (use
lchown(2)). - Have at least one smoke-test assertion in
tests/smoke-test.sh.
- Errors go through
crate::errors::PmError. Nounwrap()on user input. - All file paths are
Path/PathBuf, neverString, so non-UTF-8 paths work.
Do not mark a bug fixed on the strength of having read the diff. Before it goes in a changelog, one of these must exist:
- an assertion in
tests/smoke-test.shthat fails without the fix, or - a unit test covering the decision the fix changed, or
- a reproduction run against the built binary, pasted into the PR.
This is not process for its own sake. During the July 2026 audit review,
seal's pinhole isolation bug (§H-02) was analysed correctly, written up
correctly, and then never actually fixed — the claim reached CHANGELOG.md
and a published release because nobody ran the command. It was caught only
by a later inventory. Every other fix in that batch had a test or a
reproduction; that one had neither, and it is the one that got through.
If a fix genuinely cannot be tested in CI (it needs particular hardware, a filesystem the runner lacks, or a race), say so explicitly in the PR and in the code comment, rather than leaving the gap implicit.
- Add a variant to
Command(orAclCmd) insrc/cli.rs. - Implement the logic in a new module under
src/(or extend an existing one). - Wire the dispatch in
src/main.rs. - Add smoke-test assertions in
tests/smoke-test.sh. - Document it in
docs/janitor.1and the README command table. - Run the full Docker test suite before opening the PR.
Follow Conventional Commits:
feat(audit): add --no-group filter
fix(acl): preserve mask when restoring default ACL
docs: clarify managed-group naming scheme
The project uses a simple two-branch flow. Follow these rules to keep
dev → main merges boring:
mainonly accepts merges fromdev— never commit or cherry-pick fixes directly ontomain. Any hotfix that needs to ship must first land ondev, get tested there, then flow tomainvia merge.- Before merging
dev → main, rebasedevon top ofmain. Ifmainhas moved forward (e.g. a prior hotfix merge), runninggit fetch && git rebase origin/mainondevmakes the merge a fast-forward and eliminates conflict resolution that silently drops changes. If you cannot rebase (shareddev), at minimum mergemainback intodevfirst and resolve conflicts ondevwhere tests run. - Never amend a merge commit. If conflict resolution went wrong,
revert the merge and redo it cleanly. Amending hides the real state
behind a timestamp skew (commit-date drifting from author-date) and
the result on
maincan end up containing code that was never ondevand therefore never tested there. - Run the full test suite on the merge commit, not just on
dev. Even a clean auto-merge can combine unrelated changes in a way neither branch exercised.cargo test --release && docker run ...on the resulting merge commit is the only ground truth. - Protect
main. CI must pass on the merge commit before it is pushed. Any CI failure onmainafter a merge is a workflow bug, not just a code bug — fix it by adjusting the merge, not by piling another commit on top.
main had diverged before the merge: it carried several commits
(audit sweep, seal, scan skip pseudo-fs, …) that never landed on
dev. When the dev → main merge was made, files touched by both
sides (notably src/tree.rs, src/whocan.rs, src/render.rs,
src/presets.rs) required hand resolution. The merge commit was then
amended an hour after it was authored, so the tree on main differs
from dev's tip in 19 files — code that was never compiled or tested
together shipped under the release tag. The fix for next time is rule
2 above (rebase dev first) combined with rule 4 (test the merge
commit itself).
Please include:
janitor --version- Kernel and distro (
uname -a,/etc/os-release). - Exact command and output.
janitor --json <cmd>output if applicable.- A minimal filesystem reproducer.
Security-sensitive reports should not be filed as public issues. Use GitHub's "Report a vulnerability" feature under the Security tab.
By contributing, you agree your contributions are licensed under the MIT license.