Skip to content
Merged
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
55 changes: 51 additions & 4 deletions .github/workflows/lifecycle_drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ name: Lifecycle Drift
# complete/YYYY/MM invariant and the generated complete/index.md. Fails on:
# - lifecycle check drift (a slug in both active.md and complete.md; a
# complete/ record with no complete.md entry; a file in two states);
# - a stale complete/index.md (regenerate with `lifecycle.py index --apply`).
# - on pull requests, a stale complete/index.md (regenerate with
# `lifecycle.py index --apply`).
#
# On pushes to main a stale index is SELF-HEALED instead (issue #116): the run
# regenerates it and pushes a bot commit, and fails only if the repair itself
# fails. Mind pushes land directly on main from many concurrent agent
# sessions, so ad-hoc mutations of complete/ (e.g. a manual `git mv`) will
# keep happening — an alarm-only check emailed the human once per push until
# the next `record --apply` happened to regenerate the index.

on:
push:
Expand All @@ -24,8 +32,9 @@ on:
- "scripts/lifecycle.py"
workflow_dispatch:

# contents: write is needed by the self-heal push on main; PR runs never push.
permissions:
contents: read
contents: write

jobs:
drift:
Expand All @@ -34,5 +43,43 @@ jobs:
- uses: actions/checkout@v4
- name: lifecycle check
run: python3 scripts/lifecycle.py check
- name: index freshness
run: python3 scripts/lifecycle.py index --check
- name: index freshness (self-healing on push to main)
run: |
if python3 scripts/lifecycle.py index --check; then
exit 0
fi
if [ "${GITHUB_EVENT_NAME}" != "push" ]; then
echo "::error::complete/index.md is stale — run 'python3 scripts/lifecycle.py index --apply' on this branch and commit the result"
exit 1
fi
echo "complete/index.md is stale on main — self-healing"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Each attempt rebuilds on the current tip of main, so a concurrent
# push (the usual cause of a rejected push) never needs a rebase or
# conflict resolution — just regenerate and try again. The heal push
# uses the default GITHUB_TOKEN, which does not trigger workflow
# runs, so it cannot loop; convergence is verified before pushing
# regardless.
for attempt in 1 2 3; do
git fetch origin main
git reset --hard FETCH_HEAD
if python3 scripts/lifecycle.py index --check; then
echo "tip of main is already fresh (healed by a concurrent push)"
exit 0
fi
python3 scripts/lifecycle.py index --apply
if ! python3 scripts/lifecycle.py index --check; then
echo "::error::'index --apply' did not converge — lifecycle.py bug, repair by hand"
exit 1
fi
git add complete/index.md
git commit -m "lifecycle: self-heal stale complete/index.md"
if git push origin HEAD:main; then
echo "healed on attempt ${attempt}"
exit 0
fi
echo "push rejected (attempt ${attempt}) — retrying on the new tip of main"
done
echo "::error::could not push the healed index after 3 attempts"
exit 1