Skip to content

prompt: ship profile validation and smoke environments → complete #1335

prompt: ship profile validation and smoke environments → complete

prompt: ship profile validation and smoke environments → complete #1335

name: Lifecycle Drift
# Guards the prompt-file lifecycle (issue #71): the draft/ -> active/ ->
# 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);
# - 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:
branches: [main]
paths:
- "active/**"
- "complete/**"
- "complete.md"
- "active.md"
- "scripts/lifecycle.py"
pull_request:
paths:
- "active/**"
- "complete/**"
- "complete.md"
- "active.md"
- "scripts/lifecycle.py"
workflow_dispatch:
# contents: write is needed by the self-heal push on main; PR runs never push.
permissions:
contents: write
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: lifecycle check
run: python3 scripts/lifecycle.py 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