Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/release-health.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$comment": [
"Input to scripts/check_release_health.py. One entry per RELEASE LANE:",
"a tag namespace plus the workflow that is supposed to publish it.",
"This repository's release-and-publish.yml runs on every push to main under",
"concurrency group 'release' with cancel-in-progress: false, so GitHub",
"cancels the PENDING run whenever a third push arrives. Runs 30275153205",
"and 30276970325 are exactly that, and both were harmless because a later",
"run released the same commits. The detector is state-based for this",
"reason: it asks whether anything is still unpublished, not whether a run",
"was cancelled."
],
"repository": "OpenAdaptAI/OpenAdapt",
"branch": "main",
"ci_workflows": ["main.yml"],
"lanes": [
{
"id": "launcher",
"name": "PyPI package",
"tag_pattern": "^v(\\d+\\.\\d+\\.\\d+)$",
"release_workflows": ["release-and-publish.yml"],
"pypi_package": "openadapt",
"tracks_main": true,
"acknowledged_tags": {
"v0.36.1": "2024-07-06 legacy monorepo tag. No release object was ever created and 0.36.1 was never published to PyPI (the index jumps 0.35.x -> 0.39.1). Permanently historical: acknowledged here rather than left to alert forever, because a guard that is always red is a guard nobody reads."
},
"unreleased_grace_hours": 4,
"tag_without_release_grace_hours": 1,
"pypi_lag_grace_minutes": 30,
"remediation": "# release-and-publish.yml runs on push to main; re-run the newest run on main.\ngh run rerun --repo OpenAdaptAI/OpenAdapt \"$(gh run list --repo OpenAdaptAI/OpenAdapt --workflow=release-and-publish.yml --branch main --limit 1 --json databaseId --jq '.[0].databaseId')\""
}
]
}
140 changes: 140 additions & 0 deletions .github/workflows/release-health.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Release health

# Nothing here publishes anything, dispatches a release, or reruns one. The
# deliberate-publication design across these repositories exists because Flow
# 1.13.0/1.14.0 were yanked for shipping AGPL benchmark files. This workflow
# only LOOKS, and prints the command a human would run.
#
# Why it exists: `Release and PyPI Publish` runs 30275153205 and 30226357239
# were cancelled 22 and 81 seconds in, and nobody noticed either. This
# workflow fires on every push to main under `concurrency: release` with
# `cancel-in-progress: false`, so GitHub cancels the PENDING run whenever a
# third push arrives -- routine, usually harmless, and completely
# indistinguishable from a cancellation that dropped a release on the floor.
# `report-release-failure` below cannot tell them apart either: it only tests
# for `failure`, and a cancelled run's needs-results are `cancelled`.
#
# The sibling failure is openadapt-capture PR #51, which removed a path that
# uploaded raw audio waveforms to a third party, merged to main, and sat
# unreleased while PyPI's only installable version still contained it.
#
# Three triggers, one script:
# schedule -- the actual detector. Bounds "how long can a releasable fix
# sit unnoticed" to ~7h instead of "until someone looks".
# workflow_run -- fires when a release run COMPLETES NON-SUCCESSFULLY, which
# includes `cancelled` and `skipped`. `if: failure()` steps
# inside a release workflow cannot see either, and a run
# cancelled at a human approval gate is exactly that case.
# pull_request -- runs the detector's own offline scenarios when the
# detector changes. A detector nobody has seen fail is a
# detector nobody should trust.

on:
schedule:
# Every 3 hours. One stdlib step, no dependency install, no cache, so this
# costs seconds a day rather than a matrix. With the 4h grace window below,
# a releasable commit is surfaced within ~7h at worst.
- cron: '41 */3 * * *'
workflow_dispatch:
workflow_run:
workflows: ["Release and PyPI Publish"]
types: [completed]
pull_request:
paths:
- 'scripts/check_release_health.py'
- '.github/release-health.json'
- '.github/workflows/release-health.yml'

concurrency:
# Keyed by ref so a pull request's self-test can never queue behind (or be
# cancelled by) a scheduled main run. The check is stateless and idempotent,
# so superseding an in-flight one costs nothing.
group: release-health-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
self-test:
name: Prove the detectors fire and stay quiet
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# 3.12 explicitly: openadapt-desktop release run 29514474536 died on
# `ModuleNotFoundError: No module named 'tomllib'` on a pre-3.11 runner.
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- run: python scripts/check_release_health.py --self-test

check:
name: Detect unreleased work and silently skipped publishes
if: >-
github.event_name != 'pull_request' &&
(github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion != 'success')
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

- name: Evaluate every release lane
id: evaluate
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python scripts/check_release_health.py \
--markdown release-health.md \
--github-output "${GITHUB_OUTPUT}" \
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"

# One issue per repository, rewritten in place. A new issue every day is
# the same as no issue: it stops being read. Editing a body does not
# notify, so a persistent gap does not become a daily ping either.
- name: Open or update the single release-health issue
if: steps.evaluate.outputs.alert == 'true'
env:
GH_TOKEN: ${{ github.token }}
TITLE: "Release health - unreleased work or an incomplete publish"
run: |
set -euo pipefail
# Match on a colon-free prefix and compare the full title in jq: a
# colon inside a GitHub search phrase is parsed as a qualifier.
EXISTING=$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open \
--search "in:title \"Release health\"" --json number,title \
--jq '[.[] | select(.title == env.TITLE)][0].number // empty')
if [ -n "${EXISTING}" ]; then
gh issue edit "${EXISTING}" --repo "${GITHUB_REPOSITORY}" \
--body-file release-health.md
echo "Updated issue #${EXISTING}."
else
gh issue create --repo "${GITHUB_REPOSITORY}" \
--title "${TITLE}" --body-file release-health.md
fi

- name: Close the issue once every gap is closed
if: steps.evaluate.outputs.alert != 'true'
env:
GH_TOKEN: ${{ github.token }}
TITLE: "Release health - unreleased work or an incomplete publish"
run: |
set -euo pipefail
# Match on a colon-free prefix and compare the full title in jq: a
# colon inside a GitHub search phrase is parsed as a qualifier.
EXISTING=$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open \
--search "in:title \"Release health\"" --json number,title \
--jq '[.[] | select(.title == env.TITLE)][0].number // empty')
if [ -n "${EXISTING}" ]; then
gh issue close "${EXISTING}" --repo "${GITHUB_REPOSITORY}" \
--comment "Every release lane is published and current as of ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}. Closing automatically."
else
echo "No release-health alerts and no open issue."
fi
16 changes: 8 additions & 8 deletions platform-manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_kind": "openadapt-platform-release-manifest",
"schema_version": "1.0.0",
"generated_at": "2026-07-27T14:50:31+00:00",
"generated_at": "2026-07-27T15:42:27+00:00",
"release_channel": "beta",
"components": {
"launcher": {
Expand Down Expand Up @@ -46,21 +46,21 @@
},
"capture": {
"package": "openadapt-capture",
"version": "1.2.0",
"version": "1.2.1",
"source": "pypi",
"requires_python": ">=3.10",
"artifacts": [
{
"type": "bdist_wheel",
"filename": "openadapt_capture-1.2.0-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/ee/4c/6b5c0ab21b7300c03d22d8096b85e279def7ff7552b207ff4239e1af3108/openadapt_capture-1.2.0-py3-none-any.whl",
"sha256": "da3b2ef52b3eb24c0a565ec4c6f0eda1dcebf30be78a48b704a28d3e1316f053"
"filename": "openadapt_capture-1.2.1-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/8d/f9/fc2a6ee1f7dfec55ad97bc0cdf30ec630394283d2742a841e7ec4b70ea1b/openadapt_capture-1.2.1-py3-none-any.whl",
"sha256": "2149c87ed2a7aebeb429de3cf13bde874b98d86ab2dadff6d6444b37e93475a2"
},
{
"type": "sdist",
"filename": "openadapt_capture-1.2.0.tar.gz",
"url": "https://files.pythonhosted.org/packages/fe/98/ea22edf242430b9a15689d22c8608852db49265a356f603ec3d5bdd64595/openadapt_capture-1.2.0.tar.gz",
"sha256": "5eb5f7df329e58a15df27caba4cd3b76cf1d5779515c4c0f0cdd439631736390"
"filename": "openadapt_capture-1.2.1.tar.gz",
"url": "https://files.pythonhosted.org/packages/23/a6/4f9f3b7f928299f168feb6e004ab67ca5818e938f6262103dde636c100e8/openadapt_capture-1.2.1.tar.gz",
"sha256": "e8b9322f339ff45ffea2f58a6c89209a6c1af4fd3ea53551fbcb1e6276cf2cbe"
}
]
},
Expand Down
Loading