How to Enable Branch Coverage vs Line Coverage in CI
Line coverage counts executed lines; branch coverage checks that each conditional path is exercised.
Enable branch mode explicitly: coverage.py branch = true, Go -covermode=atomic, and Jest already tracks branches via the branches threshold. Branch coverage catches an untested else a line metric would miss.
coverage.py (branch on)
[run]
branch = true
source = ["myapp"]Jest (branch threshold)
module.exports = {
coverageThreshold: {
global: { lines: 80, branches: 80 },
},
};Gotchas
- Branch coverage is always lower than line coverage on the same code; set thresholds accordingly.
- A line can be fully covered while one of its branches is never taken, which is exactly what branch mode surfaces.
Verify it actually works
A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push
# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions:block listing every scope the job needs.- Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
actions/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.