Security¶
Multicz is a release tool: it modifies version files, writes commits,
creates tags, and (with --push) sends them to a remote. The threat
model is straightforward - the security guarantees should match.
Properties guaranteed by the implementation¶
- No network access by default. Multicz only invokes
git. There are no HTTP calls, no fetching of registries, no auto-updates. The network only enters the picture when you pass--push. - Deterministic planning. Same git history + same
multicz.tomlyields the same plan. There's no implicit time-of-day, no remote state lookup, no learned heuristic. Repeat runs are byte-identical (modulo the timestamp written intoCHANGELOG.md,debian/changelog, andstate.json, which is wall-clock UTC). - Explicit changed files from git. Multicz uses
git diff-tree --name-onlyper commit - the exact set of paths actually touched, not heuristics. Apath_overlapfinding fromvalidatereads fromgit ls-files; nothing is sniffed from a watcher or filesystem scan. - No code execution from config by default. The TOML schema is
pydantic-validated with
extra="forbid". There are no callbacks, no Python imports from data, no shell-out templates.
The single exception is post_bump: each entry is a shell command
parsed via shlex.split and executed in the repo root. As of
multicz's post_bump_policy knob, post_bump hooks are opt-in:
they only run when [project].post_bump_policy = "allow" is set.
The default is "deny", in which case hooks declared on components
are skipped and a warning surfaces on stderr pointing to the policy
knob. To disable hooks for a single run regardless of policy, pass
multicz bump --no-post-bump (the flag also silences the deny
warning). Treat enabling post_bump_policy like any other CI shell
hook — review what's there, and keep multicz.toml itself under
the same code-review process as the rest of the repo.
Hardening options¶
| concern | option |
|---|---|
| Tampered release commits | [project].sign_commits = true (details) or multicz bump --sign (passes -S to git commit) |
| Tampered tags | [project].sign_tags = true (details) or multicz bump --sign (passes -s to git tag) |
| Manual edits bypassing the bump flow | [project].state_file (details) + multicz validate (drift detection) |
| Non-conventional commits sneaking into a release | [project].unknown_commit_policy = "error" (details) |
| Overlapping component paths leaking changes silently | [project].overlap_policy = "error" (details) (default) |
| Path / mirror / trigger cycles | multicz validate - runs as a CI gate before bump |
| Shell execution from config | [project].post_bump_policy = "deny" (details) (default) - opt-in via "allow" |
CI hardening checklist¶
- Pin
multiczby exact version in your CI install step (pip install multicz==1.2.0oruv tool install --frozen multicz). - Run
multicz validate --strictfirst. It catches misconfiguredbump_files, mirror cycles, and path overlaps before anything is written. - Use
multicz plan --dry-run(ormulticz plan --output json) to inspect the bump in PR previews, not at release time. - Sign commits and tags in CI. GitHub Actions accepts a GPG key
via
crazy-max/ghaction-import-gpg; GitLab viagit config user.signingkeythen enablingsign_commits/sign_tagsinmulticz.toml. - Limit who can
--push. Multicz never pushes unless asked. Keep the release job behind a manual approval / protected branch. - Audit the state file if you've enabled it.
git log -p .multicz/state.jsongives a tamper-evident trail of every release.
The example pipelines in
examples/ci/
follow these recommendations.
Drift detection¶
When [project].state_file is set, multicz validate adds two checks:
state_drift(warning) - the recorded version doesn't match the current value in the primarybump_file. Fires when someone editspyproject.toml,Chart.yaml, orpackage.jsonmanually without going throughmulticz bump:
! api: state recorded version '1.3.0' but pyproject.toml now reads
'9.9.9' - someone may have edited the file outside multicz bump
(state_drift)
state_unknown_component(warning) - the state references a name no longer declared inmulticz.toml(typically after a component was renamed or removed without clearing state).
Treat them as errors in CI by adding --strict:
The state file is opt-in. The default stateless flow remains the recommended setup for most repos - the planner always re-derives from git, which is the source of truth.
Exit codes¶
| command | code | meaning |
|---|---|---|
validate |
0 | clean (warnings/info don't fail) |
validate |
1 | at least one error |
validate --strict |
2 | at least one warning |
bump (no commits, no --force) |
0 | "nothing to do" - success |
plan (with unknown_commit_policy = "error" and offenders) |
1 | refuses to plan, lists every offending SHA |
Use these for explicit gating - e.g. fail the pipeline on validate
warnings without merging the workflow logic with bump itself.