Skip to content

feat(repos_sync): check the hygiene conductor's repo coverage #15

feat(repos_sync): check the hygiene conductor's repo coverage

feat(repos_sync): check the hygiene conductor's repo coverage #15

Workflow file for this run

name: Spawn Drift
# Regenerates the fresh-slate templates from the live Mind/Memory and fails
# if the published PyAutoMind-template / PyAutoMemory-template repos have
# drifted from what spawn generates (the repos_sync pattern: single source,
# generated view, drift-checked). On drift, re-run spawn --write and force-
# sync the templates (the one sanctioned force-push — they are generated
# views; see docs/pyautobrain/spawn_spec.md).
on:
schedule:
- cron: "17 6 * * 1" # weekly, Monday 06:17 UTC
pull_request:
paths:
- "scripts/spawn.py"
- "tests/**"
- "docs/pyautobrain/spawn_spec.md"
- ".github/workflows/spawn_drift.yml"
workflow_dispatch:
# One sync at a time: the proposal branch is shared, so concurrent runs would
# be last-writer-wins on a force-push. Queue rather than cancel — a cancelled
# run could leave a pushed branch with no PR.
concurrency:
group: spawn-drift
cancel-in-progress: false
permissions:
contents: read
jobs:
# The privacy invariant (spawn_spec.md) is a property of the GENERATOR, so it
# is checked on every PR that touches spawn, not only on the weekly drift run.
# Hermetic — synthetic inputs only, no clones needed.
privacy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# pyyaml: the contract tests parse generated workflows. Declared here
# because setup-python gives a clean interpreter — relying on the runner
# image having PyYAML would fail collection, not just a test.
- run: pip install pytest pyyaml
- name: spawn privacy invariant + template contract
# Whole dir, not one file: a suite named in the workflow by filename
# silently stops covering anything added beside it.
run: python3 -m pytest tests/ -q
drift:
# A drift run that regenerates from a leaking generator would "fix" the
# templates by publishing instance content — so the invariant gates it.
needs: privacy
# PR runs exist to exercise the privacy job. Drift compares the PUBLISHED
# templates against Mind's main (this job clones main, not the PR head), so
# on a PR it would report unrelated pre-existing drift and go red on work
# that did not cause it.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout the live repos + published templates
run: |
for r in PyAutoMind PyAutoMemory PyAutoMind-template PyAutoMemory-template; do
git clone --depth 1 "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/PyAutoLabs/$r" "$r"
done
- name: Regenerate + diff
id: diff
run: |
mkdir published
mv PyAutoMind-template PyAutoMemory-template published/
set +e
python3 PyAutoMind/scripts/spawn.py --root "$PWD" --check published
code=$?
set -e
echo "code=${code}" >> "$GITHUB_OUTPUT"
case "$code" in
0) echo "templates are current" ;;
1) echo "::notice::templates have drifted — proposing a sync PR" ;;
2) echo "::error::spawn produced an UNSAFE tree (UNMATCHED file class or canary hit). This is a human decision: extend the spec's tables or fix the partition rules. NOT auto-healed — a canary hit means the regenerated tree carries live instance content, so a sync PR would propose publishing a leak."
exit 1 ;;
*) echo "::error::spawn --check exited ${code}, which this workflow does not understand"
exit 1 ;;
esac
# Regenerate into a clean tree and open (or refresh) one sync PR per
# drifted template repo. Deliberately a PR, not a bot push: these repos
# are force-synced generated views, so an automated push would be a
# force-push to a published `main`. #118 — a leak that sat public for
# eight days — is the argument for a human seeing what gets published.
- name: Propose the sync PR
if: steps.diff.outputs.code == '1'
env:
# GITHUB_TOKEN is scoped to PyAutoMind; writing to the template repos
# needs the org-wide PAT (same one nightly-release.yml uses).
GH_TOKEN: ${{ secrets.PAT_PYAUTOLABS }}
BRANCH: spawn/auto-sync
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::PAT_PYAUTOLABS is not set on this repo. The self-heal cannot open a PR on the template repos without a token that can write to them; GITHUB_TOKEN is scoped to PyAutoMind only."
exit 1
fi
python3 PyAutoMind/scripts/spawn.py --root "$PWD" --write regenerated
MIND_SHA=$(git -C PyAutoMind rev-parse --short HEAD)
MEMORY_SHA=$(git -C PyAutoMemory rev-parse --short HEAD)
TOP="$PWD"
opened=0
for name in PyAutoMind-template PyAutoMemory-template; do
set +e
diff -rq "regenerated/$name" "published/$name" \
--exclude .git --exclude SPAWNED_FROM >/dev/null
d=$?
set -e
# diff: 0 same, 1 differs, 2 TROUBLE (missing/unreadable tree).
# Only 1 may mean drift; 2 must abort rather than force-push.
if [ "$d" = "0" ]; then
echo "== $name: current, no PR needed"
continue
elif [ "$d" != "1" ]; then
echo "::error::diff failed ($d) comparing $name — aborting rather than replacing a tree we could not read"
exit 1
fi
echo "== $name: drifted, preparing $BRANCH"
work="work/$name"
git clone -q "https://x-access-token:${GH_TOKEN}@github.com/PyAutoLabs/$name" "$work" || {
echo "::error::cannot clone $name with PAT_PYAUTOLABS — check the token grants write to it"; exit 1; }
# Replace content wholesale; the template IS the generated tree.
( cd "$work" && find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + )
cp -a "regenerated/$name/." "$work/"
cd "$TOP/$work"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -q -B "$BRANCH"
git add -A
if git diff --cached --quiet; then
echo " nothing to commit after all"; cd "$TOP"; continue
fi
git commit -q \
-m "spawn: regenerate from mind@${MIND_SHA} memory@${MEMORY_SHA}" \
-m "Proposed automatically by the Spawn Drift self-heal (PyAutoMind#125). Review the diff before merging: merging force-syncs this generated view."
# A stable branch, force-updated: a weekly rerun refreshes the open
# PR instead of opening a new one every Monday.
git push -q -f origin "$BRANCH"
open_pr=$(gh pr list --repo "PyAutoLabs/$name" --head "$BRANCH" \
--state open --json number --jq 'length' 2>/dev/null || echo 0)
# NOT `gh pr view`: it matches merged and closed PRs too, so once a
# sync PR was merged the reused branch would report "refreshed"
# forever and silently never open another one.
if [ "${open_pr:-0}" != "0" ]; then
echo " refreshed the existing open PR"
else
body="$TOP/pr-body.md"
: > "$body"
printf '%s\n' "Opened automatically by the \`Spawn Drift\` self-heal (PyAutoMind#125)." >> "$body"
printf '%s\n' "" >> "$body"
printf '%s\n' "This repo is a **generated view** of the live Mind/Memory. It had drifted from what \`spawn.py\` produces, so this branch carries the regenerated tree." >> "$body"
printf '%s\n' "" >> "$body"
printf '%s\n' "\`spawn --check\` reported **content drift only** (exit 1). Had it reported an UNMATCHED file class or a canary hit (exit 2) no PR would exist - that is a human decision, because a canary hit means the regenerated tree carries live instance content." >> "$body"
printf '%s\n' "" >> "$body"
printf '%s\n' "Review the diff before merging: merging force-syncs this view." >> "$body"
gh pr create --repo "PyAutoLabs/$name" --base main --head "$BRANCH" \
--title "spawn: sync this generated view with PyAutoMind" \
--body-file "$body" \
|| { echo "::error::could not open the PR on $name - check PAT_PYAUTOLABS grants write there"; exit 1; }
echo " opened a new PR"
fi
cd "$TOP"
opened=$((opened + 1))
done
echo "::notice::sync PRs opened/refreshed: ${opened}"