Skip to content
Merged
Show file tree
Hide file tree
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
110 changes: 110 additions & 0 deletions .github/workflows/spawn_drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ on:
- ".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

Expand Down Expand Up @@ -58,7 +65,110 @@ jobs:
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}"
2 changes: 1 addition & 1 deletion docs/pyautobrain/spawn_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ deliberately, never silently shipped into a template.
| 8 | `skills/**`, `policy/**` | KEEP verbatim (`OWNERSHIP.md`, `create_issue/` are generic; `policy/` is org-agnostic safety text) |
| 9 | `.github/**` | **Per file, by the succeed-on-a-fresh-repo test below.** Not a blanket rule: owner substitution alone does NOT make a workflow work, because `YOURORG` is a literal placeholder — the template's own `spawn_drift` run failed `repository 'https://github.com/YOURORG/PyAutoMind/' not found`. See rules 9a–9c |
| 9a | `.github/workflows/lifecycle_drift.yml` | KEEP verbatim — operates only on its own repo (checkout + local scripts) and contains no owner reference at all, so it needs no substitution and succeeds unmodified in a fresh org. Empirically the one green workflow in the template's run history |
| 9b | `.github/workflows/spawn_drift.yml` | SPECIAL → keep with the `schedule:` trigger **stripped**. The generator machinery is generic and worth shipping, but a fresh org has no published `*-template` repos, so a weekly run would fail until it does. `pull_request` + `workflow_dispatch` remain; a comment says to re-add the schedule once templates are published |
| 9b | `.github/workflows/spawn_drift.yml` | DROP — was "keep with the `schedule:` stripped", revised in #125. The self-heal added there makes this workflow depend on `secrets.PAT_PYAUTOLABS` AND on published `*-template` repos, neither of which a freshly-spawned org has, so **every** path in it is unrunnable there and the secret reference alone breaks the no-configured-secret condition. "When in doubt DROP" applies: an org that later publishes templates can adopt this workflow deliberately, having read it. The template still ships `scripts/spawn.py` + `tests/`, so the generator and its guards travel; only the org-coupled automation does not |
| 9c | `.github/workflows/{morning_status,morning_health,arxiv_papers}.yml`, `.github/scripts/**` | DROP — instance automation. They hardcode sibling repo lists, organ-specific workflow names (`PyAutoHeart`/`PyAutoBrain`/`PyAutoHands`), org secrets (`PYAUTO_PAPERS_WEBHOOK_URL`, `CLAUDE_CODE_OAUTH_TOKEN`) and, in `arxiv_fetch.py`, strong-lensing search vocabulary plus dated incident notes. All 13 failing runs in the published template came from these |
| 9d | any other `.github/**` | **No catch-all rule — UNMATCHED by design.** A fallback here is fail-*open*: a workflow added to Mind later would ride it into the template carrying whatever schedule and secrets it has, which is precisely the defect 9a–9c fix. A new `.github` file must fail the run and get an explicit entry above, like every other new file class |
| 10 | `.claude/**`, `.codex/**` | DROP — agent-discovery symlinks are install artifacts recreated by the PyAutoBrain installer, not source content |
Expand Down
Loading
Loading