Skip to content

Release health

Release health #76

name: Release health
# Nothing here publishes anything. Releases stay `workflow_dispatch`-only by
# deliberate design -- "Releases are deliberately not triggered by every merge"
# in release.yml -- and that gate exists because 1.13.0 and 1.14.0 were yanked
# for shipping AGPL openIMIS benchmark files. This workflow only LOOKS, and
# prints the command a human would run.
#
# Why it exists: because publication is deliberate, a reviewed `fix:` or
# `feat:` can sit on green main indefinitely and nothing says so. That already
# happened next door -- openadapt-capture PR #51 removed a path that uploaded
# raw audio waveforms to a third party, merged to main, and stayed unreleased
# while PyPI's only installable version still contained it, with two
# downstream packages resolving `openadapt-capture>=1.1.0` unpinned. Flow is
# the package every other component pins, so the same gap here is wider.
#
# The publish half is the sibling failure: four openadapt-desktop tags exist
# with no release object (a failed build, an ungranted environment approval, a
# wholly cancelled run, and a missing `tomllib` on a pre-3.11 runner), and the
# launcher's release runs 30275153205 and 30226357239 were cancelled seconds
# in. `if: failure()` sees none of that: `cancelled` and `skipped` are not
# `failure`.
#
# Three triggers, one script:
# schedule -- the backstop detector. Bounds "how long can a releasable
# fix sit unnoticed" without spending eight runner starts a
# day on a deliberately human-authorized release lane.
# 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:
# Twice daily. Failed/cancelled/skipped release runs still trigger the check
# immediately through workflow_run below; this schedule only finds work for
# which nobody started a release at all.
- cron: '53 5,17 * * *'
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:
actions: read
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 }}
RELEASE_RUN_FAILED: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion != 'success' && 'true' || 'false' }}
FAILED_RELEASE_RUN_ID: ${{ github.event.workflow_run.id }}
FAILED_RELEASE_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
FAILED_RELEASE_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
set -euo pipefail
extra_args=()
if [ "${RELEASE_RUN_FAILED}" = "true" ]; then
extra_args+=(
--failed-release-run-id "${FAILED_RELEASE_RUN_ID}"
--failed-release-head-sha "${FAILED_RELEASE_HEAD_SHA}"
--failed-release-head-branch "${FAILED_RELEASE_HEAD_BRANCH}"
)
fi
python scripts/check_release_health.py \
--markdown release-health.md \
--github-output "${GITHUB_OUTPUT}" \
"${extra_args[@]}" \
--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_JSON=$(gh issue list --repo "${GITHUB_REPOSITORY}" --state all \
--limit 1000 --json number,title,state \
--jq '[.[] | select(.title == env.TITLE)][0] // {}')
EXISTING=$(jq -r '.number // empty' <<< "${EXISTING_JSON}")
EXISTING_STATE=$(jq -r '.state // empty' <<< "${EXISTING_JSON}")
if [ -n "${EXISTING}" ]; then
if [ "${EXISTING_STATE}" = "CLOSED" ]; then
gh issue reopen "${EXISTING}" --repo "${GITHUB_REPOSITORY}"
fi
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.clear == '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 \
--limit 1000 --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