post-review-verdict.sh exits 0 when it deliberately writes NO status #792
Closed
opened 2026-08-14 01:44:55 +02:00 by timothy
·
2 comments
No Branch/Tag Specified
main
renovate/lucene.net
renovate/dotnet-monorepo
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
renovate/meziantou.analyzer-3.x
renovate/cliwrap-3.x
913-scripted-playout-401
release/v26.15.0-notes
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#792
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found live during #774's own merge, and it is the failure class that repo's decision records keep naming.
scripts/post-review-verdict.sh 791 MERGEABLEprinted:and exited 0.
The fail-closed behaviour itself is CORRECT and should not change — Gitea was slow (the call ran past 120s; it was answering in ~140ms minutes later), the TOCTOU re-read failed, and refusing to write a status it could not bind safely is exactly right per
ci.verdict-write-retarget-fence.The defect is the exit code. The script did not do the thing its caller asked, and reported success. Consequences:
$?concludes the verdict was posted. It was not.Review-verdict: MERGEABLE @ <head>and reasonably infers the gate is satisfied — but the comment is not the gate, the sha-boundreview-verdict/h10status is, and it was stillpending. This isrelease.verdict-status-check's "a comment alone leaves it absent", reached by a new route.Not a security hole — the merge gate reads the STATUS, so it correctly kept denying; the system stayed fail-closed end to end. It is a lie to the operator, on the script whose entire job is making consent derivable rather than assertable.
Suggested fix
Exit non-zero on any path that posts no status. Consider also making the comment+status write atomic-ish (status first, or delete/annotate the orphaned comment on failure) so the two cannot disagree — a re-run currently leaves two identical verdict comments, which is untidy but harmless.
Done-when
curlharness inscripts/tests/test_post_review_verdict.pythat can simulate the failed re-read)Claiming this (Claude Code session, 2026-08-22), bundled with #772 — see the claim comment there. Pre-claim checks per
process.parallel-session-claimall clear: no open PR referencing it, no remote branch naming it, no prior comments, freshgit fetch origin mainat29d7a06e6.Closing record
Outcome: SHIPPED — PR #825, but not the fix this issue asked for, because the premise did not survive measurement.
Root cause: two separate things, and the issue conflated them.
dieexits 1, the usage path 2, a failingjqits own status underset -e+pipefail, a signal 128+n. The eight refusal modes are now driven through the real entry point and asserted, and those assertions pass againstorigin/main's script, which is the evidence: the defect was not in the script. The observed0came from the invocation around it (a pipeline reports its last command's status). The contract is asserted anyway — it held by convention through one shared helper and nothing had ever executed it.Review-verdict: MERGEABLE @ <head>on a PR with no gating status behind it — an artifact that reads as granted consent, produced by the run that refused to grant it.Decisions/conventions changed: added
release.verdict-writes-status-before-comment— the status is written first, the comment second, and an orphaned comment is PREVENTED rather than tolerated (the issue's third Done-when box). Rollback was the option rejected: deleting or annotating the orphan needs a Gitea call, and these refusals are usually caused by Gitea being unreachable, so the compensating write is unavailable exactly when it is needed. Ordering costs nothing and cannot fail to apply. Thedocs/remote-state-inventory.mdresidual that pointed here is closed.Reusable knowledge:
ask. Order the writes so the survivor is the safe one.Verification:
scripts/tests910 passed / 2 skipped. The ordering is proved by the predecessor split — againstorigin/main's script the 8 orphan/ordering tests go red and the 8 exit-code tests stay green. Dogfooded on this PR's own merge: the script wrotereview-verdict/h10first and the comment second, and the status was read back before merging.Deferred: none for this issue.
Docs updated: new record
docs/decisions/records/release/verdict-writes-status-before-comment.md, catalog regenerated,docs/remote-state-inventory.mdrow updated.