feat(repos_sync): check the hygiene conductor's repo coverage #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |