ci: detect unreleased work and silently skipped publishes - #1063
Merged
Conversation
abrichr
force-pushed
the
ci/release-health
branch
3 times, most recently
from
July 27, 2026 15:48
5a5af4e to
7aaca0d
Compare
`Release and PyPI Publish` runs 30275153205 and 30226357239 were cancelled
22 and 81 seconds in and nobody noticed either. report-release-failure
cannot see that: it tests only for `failure`, and every needs-result of a
cancelled run is `cancelled`.
This workflow runs on every push to main under `concurrency: release` with
`cancel-in-progress: false`, so GitHub cancels the PENDING run whenever a
third push arrives. Those cancellations are routine and usually harmless --
a later run releases the same commits -- which is precisely why an alert on
"a release run was cancelled" would be useless. The launcher already has one
guard that went that way: the platform-manifest drift check was correct, but
had gone benignly red after every single release, so real drift was
indistinguishable from noise.
So the detector is state-based. It asks whether anything is still
unpublished, not whether a run was cancelled.
scripts/check_release_health.py evaluates three detectors per release lane
declared in .github/release-health.json:
unreleased-work main carries commits past the newest release tag that
WOULD bump the version under this repository's own
[tool.semantic_release.commit_parser_options], main is
green, no run is in flight, and the oldest is past the
grace window.
tag-without-release a release tag has no release object, no run for it is
queued/waiting/running, and it is past the grace
window -- reporting failure/cancelled/skipped, or that
the workflow never started at all.
unpublished-release the newest tag's version is absent from PyPI past the
CDN grace window.
Nothing here publishes, dispatches, or reruns anything; it prints the command
a human would run. The deliberate-publication design exists because Flow
1.13.0/1.14.0 were yanked for shipping AGPL benchmark files.
--self-test proves both directions offline, including this repository's two
sharpest transients: the reconcile job's `chore(release): reconcile platform
manifest` commit that lands ~90s after every tag, and the bare `1.10.0`
release-commit subject that is not a Conventional Commit at all. Neither
bumps, so neither alerts. Runs 30275153205 and 30276970325 are in the
fixtures as the must-stay-quiet case.
The one pre-existing gap, the 2024 legacy tag v0.36.1, is acknowledged in the
config with its reason rather than left to alert forever.
Triggers: schedule every 3h, workflow_run on a non-successful release run,
and pull_request to run --self-test when the detector changes. One
stdlib-only step, no dependency install. Python pinned to 3.12 because
openadapt-desktop run 29514474536 died on a missing tomllib.
One open issue per repository, rewritten in place, closed automatically.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
openadapt-capture published 1.2.1 at 15:17 UTC -- the security-relevant release whose delay motivated the detector in the preceding commit. The platform manifest still advertised 1.2.0 and its digests, so validate_platform_manifest.py correctly went red on the next run. This is genuine cross-repository drift, not a transient: the reconcile job only runs after a LAUNCHER release, so a sibling package's release leaves this file stale until someone regenerates it. Regenerated with scripts/generate_platform_manifest.py; the only content change is the capture component's version, filenames, URLs and SHA-256 digests. python scripts/validate_platform_manifest.py OK: platform manifest validated (including published artifacts); 0 warning(s). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
abrichr
force-pushed
the
ci/release-health
branch
from
July 27, 2026 15:51
7aaca0d to
7995bf5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Release and PyPI Publishrun30275153205was cancelled 22 seconds in and nobody noticed. The same happened to30226357239the day before.report-release-failurecannot see either: it tests only forfailure, and everyneeds.*.resultof a cancelled run iscancelled.The sibling failures are the same shape from the other end —
openadapt-capturePR #51 removed a path that uploaded raw audio waveforms to a third party, merged tomain, and sat unreleased while PyPI's only installable version still contained it; and fouropenadapt-desktoptags exist with no release object at all.The design constraint that shaped this
This workflow runs on every push to
mainunderconcurrency: releasewithcancel-in-progress: false, so GitHub cancels the pending run whenever a third push arrives. Those cancellations are constant and usually harmless — a later run releases the same commits. An alert on "a release run was cancelled" would therefore be red most days and worth nothing.This repository already has one guard that went that way: the platform-manifest drift check fired correctly on genuine drift, but had gone benignly red after every single release (semantic-release bumps ~90s before reconcile; PyPI CDN lag), so
mainwas already red and the real signal was indistinguishable from noise.So the detector is state-based. It asks whether anything is still unpublished, not whether a run was cancelled.
What this is not
Nothing here publishes, dispatches, or reruns anything. It prints the command a human would run. The deliberate-publication design across these repositories exists because Flow 1.13.0 and 1.14.0 were yanked for shipping AGPL benchmark files.
What it is
scripts/check_release_health.py(stdlib only — no dependency install, no lockfile, no cache) evaluates three detectors per release lane declared in.github/release-health.json:unreleased-workmaincarries commits past the newest release tag that would bump the version under this repo's own[tool.semantic_release.commit_parser_options],mainis green, no release run is in flight, and the oldest such commit is past the grace windowtag-without-releasefailure/cancelled/skipped, or that the workflow never started at allunpublished-releaseFalse positives are the failure mode
--self-testenumerates every transient this repository actually produces and proves the detector stays quiet on each. It runs offline in CI whenever the detector itself changes.The two sharpest transients here are the
~90swindow and this repository's unusual release-commit format.reconcile-platform-manifestpusheschore(release): reconcile platform manifestafter every release —choreis inallowed_tagsbut in neitherminor_tagsnorpatch_tags, so no bump. Andcommit_message = "{version}\n\n..."means the release commit's subject is a bare1.10.0, which is not a Conventional Commit at all. Both are in the fixtures, and the first one is live onmainright now.Proof it is quiet today
Run against live
origin/main, PyPI and the GitHub API:Those "2 commits since v1.10.0" are exactly the transient:
chore(release): reconcile platform manifestandci: reduce launcher PR matrix cost (#1061).Proof it fires when it should
The same detector, unchanged, run live against
openadapt-desktop— it independently reproduced all four silently-skipped releases and their exact causes:desktop-v0.1.029514474536failuredesktop-v0.7.029756479649cancelleddesktop-v0.8.029799291231cancelleddesktop-v0.12.030169968004failureAnd replaying the exact
openadapt-capturemiss in the counterfactual where nobody looked:Triggers and cost
scheduleevery 3h — the actual detector; with the 4h grace it bounds "how long can a releasable commit sit unnoticed" to ~7h.workflow_runonRelease and PyPI Publishcompletedwith a non-successconclusion — this is the trigger that seescancelledandskipped.pull_requeston the detector's own files — runs--self-test.One stdlib step, no dependency install, no cache. Python pinned to
3.12becauseopenadapt-desktoprun29514474536died onModuleNotFoundError: No module named 'tomllib'; the script additionally degrades to documented defaults with a warning rather than crashing on an old interpreter.Issue behaviour
Exactly one open issue per repository, rewritten in place on each run (editing a body does not notify, so a persistent gap is not a daily ping) and closed automatically with a comment once every gap is closed. Never duplicated.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
Second commit:
chore(release): reconcile platform manifest with capture 1.2.1Opening this PR immediately turned
validate-platform-manifestred — and correctly so:That is not a transient and not caused by this PR.
openadapt-capturepublished 1.2.1 at 15:17 UTC — the very release whose delay motivated the detector above — andreconcile-platform-manifestonly runs after a launcher release, so a sibling package's release leavesplatform-manifest.jsonstale until someone regenerates it.main's last manifest run (30277769578, 14:59 UTC) predates the capture publish and was green; the next run onmainwill fail exactly like this one did.Regenerated with
scripts/generate_platform_manifest.py; the only content change is the capture component's version, filenames, URLs and SHA-256 digests.