GitHub Action¶
Add anonymization QA to a pull request in three lines. The action installs Visual Verifier, runs the comparison, publishes a job summary naming the failing frames, and uploads the evidence.
Published on the GitHub Marketplace.
- name: Verify anonymization coverage
uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
reference: fixtures/source.mp4
candidate: build/anonymized.mp4
The job fails when any frame is left unprotected, so a pipeline that silently skips a frame cannot merge.
What the summary looks like¶
❌ Visual Verifier — FAIL
3 of 15 frames failed. Cause: no accepted processing.
| Measurement | Value |
| Policy | generic_change_every_frame |
| Frames checked | 15 |
| Processing coverage | 80.0% |
| Frames unprotected | 3 |
| Failed frames | 4, 8, 12 |
Frame timeline
🟢🟢🟢🔴🟢🟢🟢🔴🟢🟢🟢🔴🟢🟢🟢
The timeline buckets itself on long videos, so a feature-length clip still renders as a readable strip rather than thousands of cells. A collapsed section lists tracked regions with their continuity, gaps, and recoveries.
Reviewers who want more open index.html from the uploaded artifact,
which carries a before/after comparison of every unprotected frame.
Pin the action¶
Verification thresholds are part of the result, so tracking a moving branch would let a release change a verdict without any change to your repository. Pin a tag:
Pinning the tag pins the verifier too. The requirement input defaults
to the exact package version each action tag was released with, so
@v0.3.0 installs visual-verifier==0.3.0 and keeps installing it a
year from now. Override requirement only when you deliberately want to
test a different version.
Inputs¶
| Input | Default | Meaning |
|---|---|---|
reference |
required | Original, unprocessed media |
candidate |
required | Processed media being verified |
mode |
video |
video or image comparison |
output |
visual-verifier-evidence |
Evidence directory |
options |
empty | Extra CLI options passed verbatim |
requirement |
visual-verifier==0.3.0 |
Pip requirement to install |
python-version |
3.12 |
Empty uses the runner interpreter |
targets |
empty | Reviewed target CSV of regions that must be anonymized |
target-min-coverage |
0.9 |
Fraction of a target processing must cover |
allow-uncovered-targets |
false |
Record target coverage without gating |
fail-on-gap |
true |
Gate the job on any policy failure |
upload-evidence |
true |
Upload the evidence as an artifact |
artifact-name |
visual-verifier-evidence |
Artifact name |
comment-on-pull-request |
false |
Post the summary as a comment |
github-token |
github.token |
Token used to post that comment |
options is the escape hatch for anything the action does not name. Every
command-line option is available through it:
- uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
reference: fixtures/source.mp4
candidate: build/anonymized.mp4
options: --min-severity 12 --no-annotated-video
--no-annotated-video is worth knowing about on a minimal runner. Some
container images ship an OpenCV build that cannot write MP4; verification
never needs an encoder, only the annotated evidence does.
Outputs¶
| Output | Meaning |
|---|---|
status |
PASS, FAIL, or ERROR |
exit-code |
0 passed, 1 incomplete, 2 failed |
failed-frame-count |
Frames failing the active policy |
processing-coverage-percent |
Percentage of frames with change |
target-coverage-percent |
Percentage of target frames covered |
uncovered-target-frame-count |
Target frames left uncovered |
uncovered-target-count |
Distinct targets missed at least once |
uncovered-target-ids |
Space-separated identifiers of those targets |
evidence-path |
Directory holding the evidence set |
summary-path |
Path to the generated summary.json |
Reading an output is the way to record a result without gating on it:
- name: Verify anonymization coverage
id: verify
uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
reference: fixtures/source.mp4
candidate: build/anonymized.mp4
fail-on-gap: "false"
- name: Gate on coverage instead
env:
VV_COVERAGE: ${{ steps.verify.outputs.processing-coverage-percent }}
run: awk -v value="$VV_COVERAGE" 'BEGIN { exit !(value >= 99.5) }'
Read the value through the environment rather than interpolating it into the script. That is the habit that keeps a step safe when the value comes from somewhere less trustworthy than this action.
fail-on-gap gates every policy failure
The name predates target-aware verification. fail-on-gap: "false"
suppresses gating on any completed verification that failed, so
with targets supplied it also lets an UNCOVERED_TARGETS failure
through, not only a processing gap.
That is deliberate — it is the one switch for "record the result,
do not block the merge" — but the name undersells it. A clearer
name is planned for 1.0; fail-on-gap will keep working.
To suppress only target failures while still gating on processing
gaps, use allow-uncovered-targets: "true" instead.
A run that could not complete at all, such as one with an invalid target file, fails the job either way.
Commenting on a pull request¶
The job summary needs no permissions. A comment does.
permissions:
contents: read
pull-requests: write
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
reference: fixtures/source.mp4
candidate: build/anonymized.mp4
comment-on-pull-request: "true"
The comment is rewritten in place on every run, so a long-lived pull request gets one current result rather than a column of stale ones.
Verifying an image instead¶
- uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
mode: image
reference: fixtures/source.png
candidate: build/redacted.png
Verifying reviewed targets¶
Pass a target file and the check stops asking whether anything changed and starts asking whether the required region changed:
- name: Verify anonymization coverage
id: verify
uses: SAMtheROCKET/visual-verifier@v0.3.0
with:
reference: fixtures/source.mp4
candidate: build/anonymized.mp4
targets: reviewed-plates.csv
target-min-coverage: "0.90"
A frame where two of three plates were blurred passes the generic check
and fails this one. The job summary then carries a Required targets
table naming which target was missed and where.
Target outputs let a workflow act on the detail:
- name: Open a ticket for each missed target
if: steps.verify.outputs.uncovered-target-count != '0'
env:
VV_IDS: ${{ steps.verify.outputs.uncovered-target-ids }}
run: echo "Uncovered targets: $VV_IDS"
A target file the media cannot contain — a frame number past the end of
the video, or a box extending outside the frame — fails the step with
TARGET_VALIDATION_ERROR rather than being quietly skipped. See
Target annotation.
What the action does not change¶
The action is a convenience over the exit-code contract, not a
replacement for it. It runs the same command you would run yourself, and
a PASS carries the same meaning and the same limits.
Without targets:
A
PASSmeans accepted visual change was detected in every checked frame under the configured thresholds. It does not prove that a particular required object was transformed.
With targets:
A
PASSmeans every reviewed target was covered by accepted processing in every frame it was declared on. Coverage is geometric: it does not prove the region became unreadable to a human.
How it is tested¶
action.yml is shell, so the test suite alone cannot prove it works. The
repository's own Quality workflow runs the action against both bundled
fixtures on every pull request and asserts the reported status, exit code,
failed-frame count, and coverage against the
demo contract. An untested action is a liability.