Docs Contribute to sipnab
Contribute to sipnab
Build from source, run the tests and git hooks, sign the contributor agreement, and take a change through review to merge.
On this page
- Orientation
- Contributor license agreement
- Prerequisites
- Build from source
- Running tests
- Fuzzing
- Running benchmarks
- Git hooks
- Code style
- Never publish a machine, an account, or a network
- Documentation
- Dependencies
- Commit messages
- Pull request process
- Code review
- Reporting bugs
- Security vulnerabilities
Orientation
Start with docs/architecture.md — the module map, data flow, and the “where to add things” table. Then the developer index, which is the reading order for everything below the codemap: the SIP/RTP domain primer, the subsystem guide (one packet, wire to screen), the invariants that must not break, the test tiers, ordered walkthroughs for common changes, and build/CI/release. The threading topology and lock discipline live in docs/internals/threading.md.
By participating in this project you agree to abide by the Code of Conduct.
Contributor license agreement
Sign the sipnab Contributor License Agreement before your first pull
request merges. It is a one-time step covering all of your contributions, and
you keep full ownership of your work. CLA.md holds the text;
https://sipnab.com/cla/ republishes it, and
CLA Assistant shows the same words to
signers. A gate keeps the first two copies identical, and
MAINTAINERS.md records who
re-checks the third.
Open your pull request as normal. If anyone who committed to it has not signed,
the CLAassistant bot comments within a minute or so, and you have two ways to
answer it: follow its link and authorize CLA Assistant with your GitHub account,
or post this exact sentence as a pull request comment.
I have read the CLA Document and I hereby sign the CLA
Post it verbatim – the bot matches the whole sentence, so a reworded version
reads as an ordinary comment and signs nothing. The bot then reports a
license/cla status on the pull request, and turns it green once everyone who
committed to that branch has signed.
A merge waits for it. Branch protection on main requires license/cla
alongside CI success, so a pull request cannot merge until everyone who
committed to it has signed. Bot accounts such as Dependabot cannot sign an
agreement. They are on the allowlist in the CLA Assistant settings, so their
pull requests pass the check without one.
Prerequisites
- Rust 1.99+ (edition 2024)
- libpcap headers
- macOS:
xcode-select --install - Debian/Ubuntu:
apt install libpcap-dev - Fedora/RHEL:
dnf install libpcap-devel
- macOS:
- For fuzzing only: a nightly toolchain and
cargo-fuzz(rustup toolchain install nightly && cargo install cargo-fuzz).
Build from source
# Run all of these, in order.
git clone https://github.com/NormB/sipnab.git
cd sipnab
cargo build
Running tests
The default feature set, which is the fast pass:
cargo test
Every feature-gated path, which is what CI gates on – the default build leaves
out tls, hep, api, mcp, and wasm, so their tests do not run above:
cargo test --all-features
These run the unit tests, the integration tests, the property tests
(tests/property_test.rs, proptest — SIP/SDP build→parse round-trips and
the filter-DSL total-function invariant), and the always-on smoke-fuzz
gate (tests/smoke_fuzz_test.rs, no nightly needed). The TUI has three
test tiers (insta snapshots, headless state-machine tests, and a PTY
end-to-end suite) — see
docs/internals/tui-testing.md, including
the cargo insta test --accept flow for updating snapshots.
Fuzzing
The fuzz/ crate holds 15 libFuzzer targets (nightly + cargo-fuzz).
Run one from the repository root against its seed corpus — cargo-fuzz
passes the corpus argument to the fuzz binary verbatim and never changes
its directory, so from fuzz/ it resolves to fuzz/fuzz/corpus/sip_parser
and libFuzzer exits with ERROR: The required directory ... does not exist before it fuzzes anything:
cargo +nightly fuzz run fuzz_sip_parser fuzz/corpus/sip_parser
CI compile-checks every target on each push (fuzz-check), and the
.github/workflows/fuzz.yml workflow runs the full 15-target matrix
weekly (Mondays 05:17 UTC) and on demand
(gh workflow run Fuzz -f max_total_time=300). Crash/timeout
reproducers land in fuzz/artifacts/ (git-ignored), and CI uploads them as
artifacts. Minimize one into fuzz/corpus/<parser>/ to turn it into a
regression seed.
Running benchmarks
cargo bench --profile profiling
Pass --profile profiling every time, not as an option: plain cargo bench
cannot build because the wasm cdylib crate-type forces the lib dependency
unit onto profile.release’s panic = "abort" while bench harness units
must unwind, so cargo compiles shared deps twice with incompatible type
identities (see the [lib] notes in Cargo.toml). The profiling profile is
release codegen with panic = "unwind".
Git hooks
This repo ships hooks in .githooks/. Enable them once per clone:
git config core.hooksPath .githooks
pre-commit runs nine numbered gates, starting at 0: cargo fmt --all -- --check, clippy (--features full, -D warnings), the full
cargo test --features full suite, no unwrap()/expect() or abort macro
(panic!, unreachable!, todo!, unimplemented!) in production code,
WASM exports in sync with the site’s JS, the homepage test count plus the
site version matching Cargo.toml, no TODO stubs, and an advisory
developer-docs coupling notice. Gates 0-5b block the commit. Gate 6 prints
WARN: N TODO/FIXME comments and falls through — a count, not a veto — and
gate 8 only prints REVIEW and a file list.
Gate 0 runs first because it is the cheapest check in either hook (~1.4s), so
an unformatted tree fails in seconds rather than after clippy and the whole
suite. pre-push checks formatting again — that copy is what guarantees
nothing unformatted reaches the remote — but it cannot catch the mistake early,
and a formatting slip that only surfaces at push time costs a full
commit-and-push cycle to undo.
Because gate 2 runs the whole suite, every commit takes minutes, and gate 5
means adding a test obliges you to update the count in
website/templates/index.html in the same commit.
pre-push adds thirteen hard gates, all of which mirror CI exactly and any of
which blocks the push:
| Gate | Why it is not covered by cargo test |
|---|---|
scripts/preflight.sh | Run this first. About a minute, and it checks only the things that actually bounce a commit — Vale at CI’s pinned version, codespell, both site-mirror generators, the documentation ratchets, and whether a changed test count left the homepage tile behind. On 2026-08-08 four commits bounced on exactly these at ~25 minutes each; none needed the suite to find. It does NOT run the suite, clippy, the corpus gate or the feature matrix, so a green preflight means the paperwork is right, not that the change is. A tool it cannot find — no vale, no codespell, no python3 — warns at an interactive terminal and FAILS anywhere else: under CI, with output redirected, or with PREFLIGHT_STRICT=1. PREFLIGHT_STRICT=0 keeps the warning everywhere. Automation reading “Preflight clean” from a gate that never ran is how two Vale errors reached CI on 2026-08-10. |
cargo fmt --all -- --check | Formatting is never checked by a build. |
cargo clippy --workspace --all-features --all-targets -- -D warnings | Broader than pre-commit’s --features full: also lints tests, benches, examples, and every feature-gated path. |
RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features --workspace | Rustdoc lints (e.g. private intra-doc links) build independently of the test build. |
cd fuzz && cargo check | fuzz/ is a separate workspace nothing else compiles. |
cargo check --no-default-features --features <combo> --tests over the reduced combinations | --all-features never builds a tree without native, so #[cfg] rot is invisible to it. The --tests part matters: without it no test file compiles and the gate passes over nothing. |
sh scripts/check-non-linux.sh | Re-checks a copy of the tree with the target_os values swapped, so the macOS arm of every platform split compiles here. CI is the only non-Linux build in this project, and two macOS breaks reached it on 2026-08-07 with every other gate green. Runs on Linux hosts only — on macOS or a BSD your ordinary cargo clippy already is that build, and the gate says NOT CHECKED rather than pretending. |
python3 scripts/check-yang.py | A generator writes the sipnab-diagnosis YANG module from the analysis’s tables, and a Rust test proves the committed file matches them; only a YANG implementation can say it is valid YANG. pyang --lint and yanglint compile it, pyang --check-update-from holds a new revision to the last, and yanglint -t data validates the RFC 7951 export every door writes. NOT CHECKED on a host with neither tool; CI installs both and fails without them. |
vale docs/ website/content/ README.md SUPPORT.md MAINTAINERS.md | Prose style is invisible to every cargo command. Turned main red on 2026-08-03. |
codespell over CI’s path list | Spelling likewise, and it reads src/ too — the hits that broke CI were in doc comments. |
scripts/tag-signature-check.sh on every pushed v* tag | A release tag carries the name of the maintainer who publishes it. The tag must be an annotated tag with a good SSH signature from a key in .github/allowed_signers. Nothing in cargo test sees a tag. |
CI’s full feature matrix (every combination .github/workflows/ci.yml lists,
with its RUSTFLAGS=-Dwarnings) is not in the hook: the Features job builds it
on the project’s aarch64 self-hosted runners within minutes of every push. To
see it before you push, run python3 scripts/check-feature-matrix.py, which
reads the combos and the flags out of ci.yml rather than restating them. The
reduced combinations above still catch the commonest #[cfg] rot at the push.
SKIP_FMT_HOOK=1 git push bypasses all nine — it is an emergency valve,
not a clippy-only escape, and CI runs the same gates anyway. Verify the
hooks themselves with scripts/test-pre-commit.sh and
scripts/test-pre-push.sh.
Code style
This project enforces consistent style through tooling and convention:
- Format:
cargo fmtbefore every commit. The project uses arustfmt.tomlconfig. - Lint:
cargo clippy -- -D warningsmust pass with zero warnings. - No
.unwrap()on external input. The library surface returns typedthiserrorerrors (Error,ParseError,CaptureErrorinsrc/error.rs);anyhowis for binary/app/orchestration only.clippy::unwrap_usedbans.unwrap()/.expect()on library production paths, and they are acceptable only on compile-time-known values (regex literals) or in tests.scripts/check-unwrap.pybanspanic!,unreachable!,todo!andunimplemented!there too. A site that no input can reach keeps the macro only with a// gate: <macro> because <reason>comment directly above it; the scanner reports a marker that names no reason. - Rustdoc on public types. Every
pub fn,pub struct, andpub enummust have a///doc comment. - No
unsafewithout justification. Where code needsunsafe, add a// SAFETY:comment explaining the invariant.
Never publish a machine, an account, or a network
This repository is public, and a private name that reaches main becomes
public the moment someone pushes it – removing it later leaves it in the history. It is
also, every time, worse documentation: a reader cannot resolve your hostname or
reach your LAN, so the example fails for them in a way that looks like a fault in
the tool.
Write what a reader can act on:
| Instead of | Write | Why |
|---|---|---|
a hostname (invented here: buildbox-7, sbc-east-2) | what the machine IS – the aarch64 self-hosted runner, Jetson AGX Thor, 14 cores | A benchmark needs the hardware; it never needs the box’s name. |
an address on your LAN (192.0.2.40 stands in for one here) | RFC 5737 – 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 | Reserved for documentation, and a reader can tell at a glance it is an example. |
| a global IPv6 address | RFC 3849 – 2001:db8::/32 | Same reason. |
a real domain (corp.example-isp.com) | RFC 2606 – example.com, or a .test / .invalid name | An address at a real domain reaches a real person. |
/home/you/pcaps | $HOME, /srv/pcaps, or a path relative to the repo | An absolute home path names your account and runs on one machine. |
| a gate log or a scratch file | nothing – do not commit it, and add the pattern to .gitignore | A transcript carries the paths of the machine that produced it. |
tests/private_identity_test.rs enforces all of it and tells you exactly
which line to change. One exception is allowlisted, and it is functional: the
self-hosted runner’s label on a runs-on: line in .github/workflows/ is how a
workflow reaches the one machine that can run it, and renaming it there would
not rename it on the machine.
Capture corpora are the same rule one layer down. The captures that prove this project
carry real signaling. They live outside the tree, nobody commits them, and
pages do not say where they are. A capture that IS in the tree is
public, with a source and a license, or synthetic, with the generator that
writes it, and says which: in tests/pcap-samples/PROVENANCE.md for that
directory and in tests/PROVENANCE.md for everything else.
every_committed_capture_is_public_or_synthetic finds captures by their
leading bytes, not their names, and refuses a staged one with no entry.
Documentation
Prose is US English. behavior, normalize, recognize, analyze.
the_tree_spells_in_us_english checks whole words across every tracked file –
including test files, which is where it usually catches one.
docs/ is the source of truth. Edit there. The generators build every operator page on
sipnab.com from it:
| Tree | Source of truth for | Published by |
|---|---|---|
docs/ | The in-repo docs. Read directly on GitHub. | scripts/build-wiki.py → the GitHub wiki, via wiki-sync.yml on push to main. |
website/content/docs/ | Zola content for the site. Mostly generated — do not hand-edit a page carrying the “Generated by” banner. | pages.yml on push to main. |
Regenerate after any docs change, and commit the result:
# Run all of these, in order.
python3 scripts/build-site-pages.py
python3 scripts/build-site-internals.py
site_pages_mirror_is_current re-runs both and fails if a committed mirror is
stale, so CI catches a forgotten regeneration rather than shipping it. It also
fails if a page carrying the banner is no longer written by the generator —
dropping a page from PAGES leaves its mirror on disk, still stamped
“do not edit”, quietly a hand-maintained copy again.
The filenames differ, which is why two places declare the mapping, and
they must agree — PAGES in scripts/build-site-pages.py (what the generator writes)
and DOCS_TO_SITE in scripts/build-site-internals.py (how a link to that
page is rewritten):
docs/ | website/content/docs/ |
|---|---|
cli-reference.md | cli.md |
examples.md | cookbook.md |
rest-api.md | api.md |
config-reference.md | config.md |
theme-guide.md | theme.md |
tui-walkthrough.md | tui.md |
The asymmetries are deliberate: auth.md, library.md and fault-model.md
have no site counterpart, while api-clients.md, build.md and
integrations.md are site-only and hand-maintained.
Two pages outside docs/ publish to the site the same way:
scripts/build-site-pages.py writes website/content/docs/contributing.md from
this file and website/content/docs/contrib.md from contrib/README.md, and
contributor_docs_are_generated_site_pages fails if either page is missing.
Edit the source, then regenerate.
docs/internals/ does publish to the site — ten pages under
website/content/docs/internals/, rendered by
scripts/build-site-internals.py and gated by
every_internals_page_is_published_to_the_site and site_internals_mirror_is_current
in tests/dev_docs_drift_test.rs. (Corrected 2026-08-05: this paragraph used to
list “all of docs/internals/” among the pages with no site counterpart and
call the developer docs “wiki-only by design”, contradicting the instruction
twenty lines above it to run build-site-internals.py.)
benchmarks.md is the one page that exists on both sides and is deliberately
not generated: the two copies frame the numbers differently on purpose, and
benchmark_tables_match_between_docs_and_website gates the part that must not
differ — the measured tables.
Both trees are in the flag-drift corpus in tests/docs_drift_test.rs and the
link corpus in tests/link_integrity_test.rs, so the gates check each one on its own
for phantom flags and dead links.
They are also checked against each other, and the check is a byte
comparison: site_pages_mirror_is_current in tests/dev_docs_drift_test.rs
re-runs scripts/build-site-pages.py into a
temporary directory and fails on any page whose committed output differs from a
fresh render. So the site copies in the table above are generated artifacts —
edit the docs/ source and re-run the generator. The next render
reverts a hand edit to website/content/docs/cli.md, and forgetting
to regenerate fails CI rather than passing it.
Corrected 2026-08-05: this section used to read “Nothing checks them against
each other — documenting a new flag in docs/cli-reference.md and
forgetting website/content/docs/cli.md passes every gate. That parity is yours
to keep.” Both sentences were false, and the advice they gave — hand-maintain
the generated side — was the opposite of the workflow.
Citing code from the developer docs
Pages under docs/internals/ link into the source tree, and
tests/dev_docs_drift_test.rs enforces the form:
the [`classify_packet()`](../../src/pipeline.rs) router
- Relative paths only. An absolute
github.com/NormB/sipnab/blob/main/…URL pins a branch and rots silently.build-wiki.pyrewrites the relative form into a blob URL when publishing. - Never
file:line. Line numbers are stale within a commit. A path plus a()-suffixed symbol in the link text survives a refactor, and the drift test checks that the path exists and that the symbol still has a definition. - Diagrams are mermaid
sequenceDiagram, each preceded by a prose line carrying the same point, so the page still reads where mermaid does not render. No markdown links inside a fence —build-wiki.pyrewrites links with no fence awareness and would corrupt the diagram. - Register a new page in
PAGESandGROUPSinscripts/build-wiki.py, or it never publishes to the wiki. - Add a new top-level directory to
.config/code-trees.txt. That file is the one list of trees a documentation link may point into: the wiki and site generators build their link-rewriting pattern from it, the fixerscripts/link-repo-paths.pydecides from it what it may link underdocs/internals/, and the Rust gates read it withinclude_str!.code_tree_list_matches_the_repositoryfails until the file names the new directory, because a tree missing from it is one whose links nothing rewrites and nothing checks.
The coupling rule: a change to linked code updates the page that links it, in
the same pull request. The pre-commit hook’s gate 8 prints a REVIEW list
when you stage a cited file without touching docs/internals/. It is advisory
because only you can tell whether the prose is still true. The hard gate is
dev_docs_drift_test, and it catches only the mechanical half — a link that no
longer resolves. Nothing catches prose that has quietly become wrong.
Dependencies
A dependency is a crate that sipnab’s code pulls in from someone else. Each one is code that runs with sipnab’s privileges, so adding one is a review decision, not a convenience.
Choosing a new crate
Before you add a crate, check it against these rules. cargo deny check
enforces the first three: it reads deny.toml and fails the pull
request when a crate breaks one.
- Its license is on the allow list. sipnab’s own license is
MIT OR Apache-2.0.[licenses]indeny.tomllists the licenses a dependency may carry. Adding a license to that list takes its own reviewed change, with the reason written next to it. - It comes from crates.io.
[sources]indeny.tomlrejects git dependencies and any other registry. That also rules out swapping in a patched fork of a crate: fix the problem upstream instead. - It has no open security advisory. The advisories come from the
RustSec database. The project accepts an advisory
that does not apply to sipnab only with a written reason, as the one
rsaexception indeny.tomlshows.
The reviewer checks the rest:
- Prefer what you already have. Use the standard library or a crate
already in
Cargo.lockbefore adding a new one.cargo deny checkwarns when two versions of the same crate end up in the build. - Take only the features you need. When a crate’s default features bring
in more than sipnab uses, set
default-features = falseand list the features you need, as several entries inCargo.tomldo. If only one sipnab feature needs the crate, mark itoptional = trueand let that feature turn it on. - Someone still maintains it. Look for recent releases and answered issues. Say in the pull request why you chose this crate over the alternatives.
Tracking the crates you have
- The repository commits its lock files.
Cargo.lockpins the exact version of every crate in the build. The fuzz targets form a separate workspace with their ownfuzz/Cargo.lock, also committed. - Dependabot opens update pull requests weekly for both lock files, as
.github/dependabot.ymlsets up. It groups minor and patch updates into one pull request. - CI scans every pull request to
main. TheSecurity auditjob in.github/workflows/ci.ymlrunscargo auditon both lock files andcargo deny checkon the build. A failure turns the requiredCI successcheck red, which blocks the merge. - OSV-Scanner checks every lock file on each pull request, each push to
main, and every Wednesday, from.github/workflows/osv-scanner.yml. It uses the osv.dev database, which covers more than Rust crates, and reports findings as code scanning alerts.osv-scanner.tomllists the accepted advisories, each with its reason.
When you add or update a dependency, run the same checks CI runs before you
push. Install the tools once with cargo install cargo-audit cargo-deny.
# Run all of these, in order.
cargo audit --ignore RUSTSEC-2023-0071
cargo audit --file fuzz/Cargo.lock --ignore RUSTSEC-2023-0071
cargo deny check
Updating vendored files
A few files come from other projects’ releases, copied into the repository
rather than fetched by a package manager. Dependabot and cargo audit do not
see them, so nothing tells you when a new version comes out. Each one has a row in
the “Vendored files” table of
THIRD-PARTY-NOTICES.md naming its version, where
it came from, its license and its SHA-256.
| File | What it is | Where a new version comes from |
|---|---|---|
website/static/js/mermaid.min.js | Mermaid, which draws the site’s diagrams | dist/mermaid.min.js in the mermaid package on the npm registry |
website/static/js/scalar.min.js | Scalar, which renders the REST API reference | dist/browser/standalone.js in the @scalar/api-reference package on the npm registry |
tests/schemas/publisher/vcon_json_schema.json | The vCon working group’s JSON schema, a test fixture | vcon_json_schema.json at a commit of draft-ietf-vcon-vcon-core |
To update one:
- Download the new release and copy the file over the old one unchanged. For
a package on the
npmregistry,npm pack <package>@<version>downloads the release tarball without installing anything. - In
VENDOREDinscripts/build-third-party-notices.py, change the file’s version and SHA-256.sha256sum <file>prints the new hash. - Regenerate the notices with
python3 scripts/build-third-party-notices.pyand commit both files. - For the vCon schema, also change
VCON_PUBLISHER_COMMITandVCON_PUBLISHER_SHA256intests/json_schema_test.rs. A test there then checks that sipnab’s owntests/schemas/vcon.schema.jsonstill differs from the new file only where it documents a deviation.
every_vendored_file_is_recorded_with_its_version in
tests/docs_drift_test.rs fails when a file’s
hash is not the one recorded, or a script’s recorded version is not the one
the script itself contains. A new minified script under website/static/js/
also fails it until it has a row.
Commit messages
Use Conventional Commits format:
feat: add --nat-issues diagnostic alias
fix: handle empty Contact header without panic
docs: update CLI reference with new output flags
refactor: extract SDP parser into its own module
test: add pcap round-trip tests for IPv6
Pull request process
- Fork the repository and create a feature branch from
main. - Keep changes focused – one logical change per PR.
- Ensure the CI gate passes locally. Beyond
cargo fmtandcargo test --all-features, CI enforces:cargo clippy --workspace --all-features --all-targets -- -D warnings- a reduced-feature matrix that must compile (
native,tls,api,mcp,hep,tls,api,native,tui,audio,native,tui,tls,hep,api,native,hep,api,mcp,mcp-http,wasm) - a docs gate
(
RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features) cargo audit+cargo denyfuzz-check(the fuzz targets must compile on nightly)
- Add or update tests for new functionality.
- Update documentation if you add or change CLI flags or config keys.
- Describe the “why” in the PR body, not just the “what”.
Code review
A reviewer reads every change to main before it merges. This section says
who reviews it, how, what the review checks, and what a change needs before it
can merge. Where a setting or a file enforces a rule rather than a person, the
rule names it.
Who reviews
The maintainer listed in MAINTAINERS.md reviews every pull
request. .github/CODEOWNERS assigns the whole tree
(* @NormB), so GitHub requests that review automatically when a pull request
opens.
sipnab has one maintainer today, so nobody else can review changes the
maintainer writes. Those changes go through the same pull request, the same
checklist and the same required checks as a contribution from anyone, and the
maintainer reviews the diff before merging. Branch protection on main therefore requires zero approving
reviews: requiring one would block every change the only maintainer makes.
When a second maintainer joins (see
Getting commit access), the
requirement becomes one approving review from someone other than the author.
How a review works
- Through a pull request only. Branch protection on
mainrequires a pull request and applies to administrators too, so nobody pushes tomaindirectly.tests/branch_protection_drift_test.rsfails if that setting and this documentation disagree. - Against the checklist. The reviewer works through the checklist in
.github/PULL_REQUEST_TEMPLATE.mdand the points below. - In the pull request’s conversation. Questions and requested changes go in review comments. Branch protection requires every conversation to be resolved before the pull request merges. A new push dismisses an earlier approval, so an approval always covers the code that merges.
What the reviewer checks
- Correctness, with tests. The change does what its description says, and it comes with a test that fails without the change and passes with it.
- Documentation. The change updates the docs for any new or changed flag, config key or behavior, as Documentation describes.
- Security impact. The reviewer asks which trust boundary in the threat model the change touches, such as capture input, HEP senders, API clients or plugins, and whether it weakens the checks listed there. A change that moves a boundary updates that page.
- Dependencies. A new or updated crate meets the rules in
Dependencies.
cargo deny checkenforces some of them, and the reviewer checks the rest. - No secrets and no private names. Nothing in the diff is a token, a
password, or a private hostname, address or path. The repository has
GitHub secret scanning with push protection turned on, and
tests/private_identity_test.rsenforces the rules in Never publish a machine, an account, or a network. - Public claims match the code. Anything the change says in the README, the site or the docs is true of the code as merged.
- A changelog entry. A user-visible change adds an entry under
## [Unreleased]in CHANGELOG.md. - The contributor agreement. Everyone who committed to the branch has signed the CLA.
What a change needs before it can merge
A pull request is acceptable when all of these hold:
- The required checks are green. Branch protection on
mainrequiresCI successandlicense/cla, and requires the branch to be up to date withmainbefore it merges.CI successpasses only when every CI job it depends on passes.license/clapasses when everyone who committed to the branch has signed the CLA. - Every commit carries a signature. Branch protection on
mainrequires signed commits, so GitHub refuses a merge that contains an unsigned or unverified one. - No review conversation stays open. Branch protection blocks the merge until someone marks each one resolved.
- The reviewer agrees that the change meets the pull request template checklist and the points under What the reviewer checks. The contributor says so in the template checklist, and the reviewer confirms it.
Reporting bugs
Open a GitHub issue with:
- sipnab version (
sipnab --version) - OS and architecture
- Steps to reproduce
- Expected vs. actual behavior
- A pcap or SIP trace if applicable (sanitize credentials first)
Security vulnerabilities
Do not open a public issue. See SECURITY.md for responsible disclosure instructions.