Skip to content

v2.5.1

Why this patch exists at all

I noticed something weird about the version cadence: every release since v2.0.1 was a minor bump cut by hand. v2.0.1 was the last automatic patch, four days ago. After that, five separate fix commits landed on main and none of them triggered the patch publish that normally runs on every CI-green push.

That is a real outage of the release pipeline, not a stylistic choice. This patch is the catch-up.

What broke the auto patch publish

The post-CI dispatcher (the workflow_run listener that fans out into auto-release, auto-heal, bisect-on-red, telegram-notify, bernstein-ci-fix) returned startup_failure on every run for the last 16 hours. 20+ consecutive failures, all silent, because the dispatcher is itself triggered by another workflow and the GitHub UI does not surface dispatcher failures the same way it surfaces a failing PR check.

Root cause: the Tier-3 shadow-mode PR (#1715) added secrets.OPENROUTER_API_KEY_FREE to the dispatcher's secrets: block for the call into bernstein-ci-fix.yml. The receiving workflow declares that secret required: false, so it should have been optional. But GitHub Actions resolves the secrets: block of a reusable workflow at call-start, before the called workflow's required: false declaration applies. The operator had not defined that secret on the repo. Result: every dispatcher call fails at startup, every downstream workflow including auto-release stays silent.

The fix in #1730 is one line: drop the pass with a NOTE comment recording how to restore it once the operator defines the secret. After that lands, the next CI-green push on main runs the dispatcher cleanly, auto-release computes the conventional-commits delta since the last tag, and the next patch publishes itself.

What ships in this patch

Five fix PRs that should have been patch publishes one by one:

  • fix(security) (#1705) resolved 12 CodeQL / Semgrep / workflow code-scanning alerts and dismissed 23 false-positives with per-alert technical justification. Real classes hit: defusedxml for the S3 XML parse, html.escape on the OIDC / SAML success pages, dropped exception text from the telemetry 400 responses, a linear-time regex for _AGED_BLOCK_RE to close the ReDoS, and chmod 0o600 on the audit-log files that had been world-writable.

  • fix(observability) (#1713) repaired the Sonar workflow_run skip race. Every recent main CI run completed with conclusion=cancelled due to the cancel-in-progress: true concurrency group, and the Sonar workflow's if: workflow_run.conclusion == 'success' short-circuited to skipped. The fix adds a direct push: branches: [main] trigger with a thin coverage fallback when the upstream artifact is missing. First non-skipped scan in days landed immediately.

  • fix(privacy) (#1718) scrubbed the last operator-private hostnames from publishable surfaces: three docs files were using the real hosts in worked examples, the Sonar PR-comment template rendered the host into PR comment bodies, and two workflows had hosts as vars.X || 'real-host' fallbacks. All replaced with *.example.com placeholders in docs, dropped from the PR-comment template, and the workflow fallbacks now read from vars.* with no hardcoded backup. The regression test (added in v2.5.0) was extended to scan docs/ and .github/workflows/ alongside src/.

  • fix(routes) (#1726) replaced sync subprocess.run calls inside three async route handlers with asyncio.create_subprocess_exec so a 200ms-1s subprocess no longer parks the whole event loop, and narrowed eight bare except Exception: clauses in the routes layer. Three of the eight were intentionally broad and got a bot-ack: intentional-broad-except comment plus sanitize_log routing for the traceback. A new CI lint refuses any future bare-except in src/bernstein/core/routes/ without that comment.

  • fix(api) (#1727) caught what looked like a Schemathesis flake and was actually a real intermittent 500. TaskCreate.scope and complexity were typed plain str with a length cap only. An empty or out-of-range value passed pydantic and then raised ValueError deep in the task store when Scope(value) / Complexity(value) constructed the enum. Schemathesis hit it only on the unlucky inputs, so everyone kept rerunning the job. The fix validates at the request boundary and returns 422. Same PR also caps GET /tasks at 500 on the legacy unparameterised path (the unpaginated response had no upper limit) and pushes the claim_batch tenant check inside the store lock to close the prior TOCTOU between authz and claim.

What I am not claiming

The dispatcher fix is the minimum to get the chain firing again; the longer term answer is to convert the secrets-block pass into a vars.*-gated conditional so an undefined optional secret on the calling side cannot silently break the entire downstream pipeline. That hardening is a separate PR.

The bot-ack comment convention for intentional broad-excepts is a new pattern; if it turns into noise, the new CI lint can be downgraded to advisory.

Operator follow-up

If you want Tier-3 OpenRouter shadow mode (#1715) to actually run, define OPENROUTER_API_KEY_FREE on the repo secrets and restore the pass per the NOTE in post-ci-dispatcher.yml. The shadow mode otherwise stays correctly gated off and the dispatcher works fine without it.

Try it

pipx install --upgrade bernstein

Source: https://github.com/sipyourdrink-ltd/bernstein (Apache-2.0). Full per-PR notes in docs/release-notes/v2.5.1.md. 6 commits on main since v2.5.0.