Introduction
When your test suite runs across several parallel CI runners (say, sharded PHPUnit groups or a matrix job), each runner only produces coverage for the slice of code it executed. Uploading those slices as separate flags or components would fragment the results and throw off the totals.
Coverage parts tell OtterWise that an upload is part N of M for a given flag and component. OtterWise waits until every part has arrived before posting status checks, PR comments, or totals, and then merges them into one report.
When to use parts vs. flags vs. components
- Parts: the same test suite executed across multiple runners (parallel sharding).
- Flags: different test suites for the same code, like unit vs. integration.
- Components: different sub-projects in a monorepo, like a frontend and a backend.
Uploading Parts
Each runner invokes the bash uploader with two extra flags:
| Option | Description |
|---|---|
--part |
The index of this upload (1-based, must be a positive integer). |
--part-total |
The total number of parts that will be uploaded for this flag and component. Must match across all parts. |
Both flags default to 1, so if you only upload once you never need to set them.
Example: 3-way Parallel PHPUnit
# Runner 1 bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./coverage/shard-1.xml \ --part 1 \ --part-total 3 # Runner 2 bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./coverage/shard-2.xml \ --part 2 \ --part-total 3 # Runner 3 bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./coverage/shard-3.xml \ --part 3 \ --part-total 3
Example: Parts Combined with Flags and Components
Parts are scoped to a flag and component pair, so you can shard one suite while still uploading other suites as single uploads:
# Unit tests (2 parallel shards) on the backend component bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./backend/coverage/unit-shard-1.xml \ --component backend --flag unit-tests \ --part 1 --part-total 2 bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./backend/coverage/unit-shard-2.xml \ --component backend --flag unit-tests \ --part 2 --part-total 2 # Integration tests (single upload) on the same component bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --file ./backend/coverage/integration.xml \ --component backend --flag integration-tests
GitHub Actions Example
On GitHub Actions the shard index and total come from a matrix strategy:
jobs:
test:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: ./vendor/bin/pest --parallel --processes=4 --shard=${{ matrix.shard }}/4
- name: Upload coverage to OtterWise
env:
OTTERWISE_TOKEN: ${{ secrets.OTTERWISE_TOKEN }}
run: |
bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \
--file ./coverage/clover.xml \
--part ${{ matrix.shard }} \
--part-total 4
How OtterWise Processes Parts
Each upload is stored as its own CommitCoverage row, keyed by (commit, flag, component, part_index). Commit-level totals are recomputed after every part arrives, but the commit is only treated as fully covered once the received part count matches part_total.
Until the final part arrives:
- Status checks, PR comments, and line annotations are deferred, so reviewers see one merged result instead of a stream of partial ones.
- The commit's
has_coverageflag staysfalse, so the dashboard won't surface partial numbers. - Carry-forward from the default branch is not triggered. The incomplete parts are still valid inputs.
Uploads are idempotent. Re-sending the same --part value (for example, after a retried CI step) overwrites the previous row for that part rather than double-counting.
Rules
--part-totalmust match across all parts. If runner 1 sends--part-total 3but runner 2 sends--part-total 4, OtterWise uses the highest value seen and keeps waiting for the missing parts.- Each
--partindex should be unique per flag+component. Two uploads with--part 1will overwrite each other. - Parts are scoped per flag and component. An upload with
--flag unit-tests --part 1/2has nothing to do with one using--flag integration-tests --part 1/2; each pair must complete on its own. - If one runner fails to upload, OtterWise will not post a PR comment or status check for that commit until the missing part is uploaded (or until a later commit arrives with complete coverage).
- Values must be positive integers.
--part 0, negative values, and values greater than--part-totalare rejected by the uploader.
Troubleshooting
| Symptom | Cause & Fix |
|---|---|
| PR comment / status check never appears |
At least one part didn't upload. Check each runner's CI logs for the OtterWise uploader output. Re-run the failing shard with the same --part value to complete the set.
|
| Coverage numbers look too low |
Likely a --part-total mismatch or a missing part. Verify every runner uses the same --part-total value and that indices 1 through N are each uploaded exactly once.
|
| Uploader exits with "--part must be a positive integer" | The matrix variable was empty or non-numeric. Echo the value before invoking the uploader to confirm it's set. |
| Uploader exits with "--part cannot be greater than --part-total" |
Your shard indexes exceed the declared total. Update --part-total to match the number of shards.
|
Related
- Bash Uploader: full list of options.
- Pull Request Comments: deferred until all parts arrive.
- Status Checks: posted once coverage is complete.