From dcce212b60e908eb6c99cc693554306c515eab6c Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 26 Aug 2026 12:58:32 -0400 Subject: [PATCH] feat(sweep): enumerate the org for repo settings, not the body map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A body-map-derived target set is blind in exactly the wrong place: a repo is created *before* anyone registers it, so the repos most in need of "delete the head branch on merge" were the ones repo_settings.yml could not see. Nine live org repos were absent from the body map entirely, and ten more fell outside the sweepable categories — a boundary drawn around *deleting refs*, which has no bearing on flipping a reversible boolean. The sweep now lists the organisation from the GitHub API (non-archived repos under the workflow's own owner), so a new repo is covered the run after it exists and no category question arises. `--outside-owner` supplies the complement an org listing cannot return: body-map repos under another account. Because those sit under a personal account whose PAT admin scope is unverified by design, the failure policy is now scope-aware — an in-org refusal is an error, an out-of-org one a warning. Without that split the weekly unattended run would go red permanently the first time it touched one. `--include-self-sweeping` is retired: org enumeration removed its only consumer, and an unused widening flag on a boundary whose other modes delete refs is a trap. The category boundary itself is unchanged — `targets()` still yields the same 25 slugs for branch_sweep_all.yml and branch_archive.yml. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EqUT9Wc6mVLiJmbcstT7Z3 --- .github/workflows/repo_settings.yml | 92 +++++++++----- bin/branch_sweep_targets.py | 71 +++++++---- skills/repo_cleanup/SKILL.md | 2 +- skills/repo_cleanup/reference.md | 7 ++ tests/test_branch_sweep_targets.py | 188 ++++++++++++++++++++-------- 5 files changed, 253 insertions(+), 107 deletions(-) diff --git a/.github/workflows/repo_settings.yml b/.github/workflows/repo_settings.yml index adb2f0af..3908d776 100644 --- a/.github/workflows/repo_settings.yml +++ b/.github/workflows/repo_settings.yml @@ -1,7 +1,7 @@ name: Repo Settings -# Turns on "Automatically delete head branches" across the organism's -# development repos, and reports any repo where it is off. +# Turns on "Automatically delete head branches" across the organism's repos, +# and reports any repo where it is off. # # WHY THIS EXISTS. Branch cleanup was written as a close-out step: /prm deletes # the head once the PR merges. That step cannot run in a cloud Claude session — @@ -14,14 +14,23 @@ name: Repo Settings # workflow is the fix: with the setting on, GitHub removes each head at merge # and no session credential is involved at all. It is a *per-repo* setting with # no org-wide default, so something has to visit each repo — that something is -# here, driven off the body map, rather than a browser tab per repo. +# here rather than a browser tab per repo. +# +# WHY IT ENUMERATES THE ORG RATHER THAN THE BODY MAP. It derived its targets +# from the body map first, and that was blind in exactly the wrong place: a +# repo is created *before* anyone registers it, so the repos most in need of +# the setting were the ones the sweep could not see — along with any repo whose +# category the body map excludes for the branch sweep's sake, a boundary drawn +# around *deleting refs* that has no bearing on flipping a boolean. Listing the +# organisation instead means a new repo is covered the run after it exists, and +# no category question arises. `--outside-owner` supplies the complement: the +# body-map repos under some other account, which an org listing cannot return. # # WHY THE SCHEDULE APPLIES RATHER THAN AUDITS. Unlike the branch sweep, this # deletes nothing: it flips a boolean whose effect starts at the *next* merge, # and it is one click to undo. Left as audit-only it would report the same # drift every week and rely on someone acting on it — which is the failure this -# is meant to end. A repo added to the body map after today is corrected on the -# next run, the same way dashboard_refresh.yml self-heals a stale page. +# is meant to end. on: workflow_dispatch: @@ -47,12 +56,12 @@ jobs: settings: runs-on: ubuntu-latest steps: - - name: Check out PyAutoBrain (the target derivation lives here) + - name: Check out PyAutoBrain (the complement derivation lives here) uses: actions/checkout@v4 with: fetch-depth: 1 - - name: Check out the Mind (the body map the targets derive from) + - name: Check out the Mind (the body map the complement derives from) uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/PyAutoMind @@ -69,71 +78,94 @@ jobs: # A repo's own GITHUB_TOKEN reaches only that repo, and this setting # needs admin on each one. Same credential the org-wide sweep uses. GH_TOKEN: ${{ secrets.PAT_PYAUTOLABS }} + # Passed as env rather than interpolated into the script below, so a + # value containing shell metacharacters cannot become shell. + ORG: ${{ github.repository_owner }} + MODE: ${{ inputs.mode }} run: | set -uo pipefail - mode='${{ inputs.mode }}' - mode="${mode:-apply}" + mode="${MODE:-apply}" if [ -z "${GH_TOKEN:-}" ]; then echo "::error::PAT_PYAUTOLABS is not set — cannot reach sibling repos." exit 1 fi - # Every development repo, the self-sweeping organs included: the - # setting is per-repo and they merge pull requests like anything else. - targets=$(python3 bin/branch_sweep_targets.py --include-self-sweeping .mind/repos.yaml) - if [ -z "$targets" ]; then - echo "::error::no repos derived from the body map — refusing to continue." + # Every repo in the organisation, archived ones excluded: an archived + # repo takes no merges, and PATCHing one fails. + in_org=$(gh api --paginate "orgs/$ORG/repos?per_page=100" \ + --jq '.[] | select(.archived == false) | .full_name') + if [ -z "$in_org" ]; then + # Distinguishable from "nothing to do": an empty listing means the + # PAT lost org read, and sweeping nothing would look like success. + echo "::error::listed no repos in $ORG — refusing to continue." exit 1 fi + # The complement: body-map repos an org listing cannot return. + outside=$(python3 bin/branch_sweep_targets.py --outside-owner "$ORG" .mind/repos.yaml) + { echo "## Automatically delete head branches — \`$mode\`" echo - echo "| Repo | Before | Action |" - echo "|---|---|---|" + echo "| Repo | Scope | Before | Action |" + echo "|---|---|---|---|" } >> "$GITHUB_STEP_SUMMARY" failed=0 changed=0 already=0 - for slug in $targets; do + + visit() { + slug="$1" scope="$2" + # Out-of-org repos sit under a personal account. The org PAT's + # admin scope there is unverified by design, so a refusal is + # expected and must not redden a weekly unattended run; in-org, the + # same refusal is a real finding. + case "$scope" in + in-org) hard=1 ;; + *) hard=0 ;; + esac + before=$(gh api "repos/$slug" --jq '.delete_branch_on_merge' 2>/dev/null) case "$before" in true) already=$((already + 1)) - echo "| \`$slug\` | on | — |" >> "$GITHUB_STEP_SUMMARY" - continue ;; + echo "| \`$slug\` | $scope | on | — |" >> "$GITHUB_STEP_SUMMARY" + return ;; false) ;; *) echo "::warning::$slug — could not read the setting (repo missing, or the PAT cannot see it)" - echo "| \`$slug\` | ? | **unreadable** |" >> "$GITHUB_STEP_SUMMARY" - failed=1 - continue ;; + echo "| \`$slug\` | $scope | ? | **unreadable** |" >> "$GITHUB_STEP_SUMMARY" + [ "$hard" -eq 1 ] && failed=1 + return ;; esac if [ "$mode" = "audit" ]; then - echo "| \`$slug\` | off | would enable |" >> "$GITHUB_STEP_SUMMARY" + echo "| \`$slug\` | $scope | off | would enable |" >> "$GITHUB_STEP_SUMMARY" changed=$((changed + 1)) - continue + return fi # -F, not -f: the API wants a JSON boolean, not the string "true". if gh api -X PATCH "repos/$slug" -F delete_branch_on_merge=true --silent 2>/dev/null; then echo "✓ $slug — enabled" - echo "| \`$slug\` | off | **enabled** |" >> "$GITHUB_STEP_SUMMARY" + echo "| \`$slug\` | $scope | off | **enabled** |" >> "$GITHUB_STEP_SUMMARY" changed=$((changed + 1)) else # The read succeeded, so this is a permission ceiling, not a typo: # changing a repo setting needs admin, which read access does not - # imply. Widening the PAT is a human decision, not a retry. + # imply. Widening a PAT is a human decision, not a retry. echo "::warning::$slug — PATCH refused; the PAT needs admin on this repo" - echo "| \`$slug\` | off | **refused (needs admin)** |" >> "$GITHUB_STEP_SUMMARY" - failed=1 + echo "| \`$slug\` | $scope | off | **refused (needs admin)** |" >> "$GITHUB_STEP_SUMMARY" + [ "$hard" -eq 1 ] && failed=1 fi - done + } + + for slug in $in_org; do visit "$slug" in-org; done + for slug in $outside; do visit "$slug" outside; done { echo echo "$already already on · $changed $([ "$mode" = audit ] && echo 'to enable' || echo 'enabled')" } >> "$GITHUB_STEP_SUMMARY" - [ "$failed" -eq 0 ] || { echo "::error::one or more repos could not be read or set — see the table"; exit 1; } + [ "$failed" -eq 0 ] || { echo "::error::one or more repos in $ORG could not be read or set — see the table"; exit 1; } diff --git a/bin/branch_sweep_targets.py b/bin/branch_sweep_targets.py index 11acb71d..370494f4 100644 --- a/bin/branch_sweep_targets.py +++ b/bin/branch_sweep_targets.py @@ -32,16 +32,27 @@ credentials. "Mind" and "Brain" are organism roles present in any fork, so naming them here is not an instance fact. -The self-sweeping exclusion is the *sweep's* boundary, not the body map's, so -a second consumer can ask for it back. `--include-self-sweeping` yields every -development repo including the Mind and the Brain — what the repo-settings -sweep wants, because "delete the head branch on merge" is a per-repo setting -that has to be on in those two as much as anywhere else, and no second sweeper -collides over a settings PATCH. +## The second consumer reads the body map for its *complement* + +`repo_settings.yml` turns on "delete the head branch on merge". It used to take +a widened version of the set above, and that was wrong in a way the category +boundary cannot fix: a repo is created *before* anyone registers it in the body +map, so a body-map-derived sweep is structurally blind to exactly the repos +that most need the setting. It now enumerates the organisation from the GitHub +API instead, where a new repo appears the moment it exists. + +What that enumeration *cannot* see is a body-map repo under some other owner. +`--outside-owner ` yields those, and only those — the complement of the +org listing. It applies **no category filter**, deliberately: the org +enumeration has none either, so filtering the complement would be a boundary +that exists on one side of the union and not the other. (In practice the +categories that end up outside an organisation's own account are the personal +ones, which the category set above excludes — so filtering here would yield +nothing at all and silently drop the repos this flag exists to find.) Usage: branch_sweep_targets.py # one owner/repo per line - branch_sweep_targets.py --include-self-sweeping + branch_sweep_targets.py --outside-owner """ from __future__ import annotations @@ -61,32 +72,48 @@ SELF_SWEEPING_ORGANS = frozenset({"Mind", "Brain"}) -def targets(body_map: dict, include_self_sweeping: bool = False) -> list[str]: - """The development `owner/repo` slugs, in body-map order. - - `include_self_sweeping` keeps the organs that host their own branch - sweeper. Only the branch sweep needs them dropped (two sweepers on one - repo would contend); a consumer that flips a repo setting does not. - """ +def targets(body_map: dict) -> list[str]: + """The development `owner/repo` slugs, in body-map order.""" out = [] for entry in body_map["repos"].values(): if entry.get("category") not in SWEEPABLE_CATEGORIES: continue - if not include_self_sweeping and entry.get("organ") in SELF_SWEEPING_ORGANS: + if entry.get("organ") in SELF_SWEEPING_ORGANS: continue out.append(entry["github"]) return out +def outside_owner(body_map: dict, owner: str) -> list[str]: + """Body-map slugs whose owner is not `owner`, in body-map order. + + The complement of a `GET /orgs//repos` listing: what a settings + sweep would miss if it trusted the API enumeration alone. No category + filter — see the module docstring for why that asymmetry is deliberate. + """ + return [ + entry["github"] + for entry in body_map["repos"].values() + if entry["github"].split("/", 1)[0] != owner + ] + + def main(argv: list[str]) -> int: args = argv[1:] - include_self_sweeping = False - if "--include-self-sweeping" in args: - include_self_sweeping = True - args = [a for a in args if a != "--include-self-sweeping"] + owner = None + if "--outside-owner" in args: + i = args.index("--outside-owner") + # The flag needs a value, and the value must not be the body-map path + # arriving by accident — a missing value would otherwise swallow it and + # report "no repos outside ", which reads as a clean empty run. + if i + 1 >= len(args) or args[i + 1].startswith("-"): + print("branch_sweep_targets: --outside-owner needs an owner", file=sys.stderr) + return 2 + owner = args[i + 1] + args = args[:i] + args[i + 2 :] if len(args) != 1 or args[0].startswith("-"): print( - f"usage: {Path(argv[0]).name} [--include-self-sweeping] ", + f"usage: {Path(argv[0]).name} [--outside-owner ] ", file=sys.stderr, ) return 2 @@ -96,7 +123,9 @@ def main(argv: list[str]) -> int: if not path.is_file(): print(f"branch_sweep_targets: no body map at {path}", file=sys.stderr) return 1 - for slug in targets(yaml.safe_load(path.read_text()), include_self_sweeping): + body_map = yaml.safe_load(path.read_text()) + slugs = outside_owner(body_map, owner) if owner is not None else targets(body_map) + for slug in slugs: print(slug) return 0 diff --git a/skills/repo_cleanup/SKILL.md b/skills/repo_cleanup/SKILL.md index e6e5c362..065ac3aa 100644 --- a/skills/repo_cleanup/SKILL.md +++ b/skills/repo_cleanup/SKILL.md @@ -130,6 +130,6 @@ Format: [`reference.md`](reference.md) → "Recap". - A repo that still accumulates merged PR heads has **"Automatically delete head branches" off** (Settings → General). That setting prevents the backlog this skill exists to clear, and no sweep substitutes for it — dispatch - `repo_settings.yml` (mode `apply`) to turn it on body-map-wide, then move on. + `repo_settings.yml` (mode `apply`) to turn it on org-wide, then move on. - Cloud-session routing (dispatch `branch_sweep.yml` rather than pushing deletes) is in [`reference.md`](reference.md) → "Execution environments". diff --git a/skills/repo_cleanup/reference.md b/skills/repo_cleanup/reference.md index cf1c2c38..9e30d2f3 100644 --- a/skills/repo_cleanup/reference.md +++ b/skills/repo_cleanup/reference.md @@ -170,6 +170,13 @@ Never-touched repos fall out with their categories — `autolens_assistant` is a names them twice. Brain code may not name satellite repos at all (the tenant firewall enforces this; a hardcoded list was written first and rejected). +That boundary is this sweep's alone. `repo_settings.yml` no longer shares it: +flipping `delete_branch_on_merge` is not deleting a ref, and a body-map-derived +target set could never see a repo in the window between its creation and its +registration — so it lists the organisation from the GitHub API instead, and +uses `--outside-owner` only for the body-map repos an org listing cannot +return. + ## Branches that must not simply be deleted A branch the sweep classes as *unmerged* is not automatically live work. It may diff --git a/tests/test_branch_sweep_targets.py b/tests/test_branch_sweep_targets.py index b1d6e614..32cfbe60 100644 --- a/tests/test_branch_sweep_targets.py +++ b/tests/test_branch_sweep_targets.py @@ -1,8 +1,11 @@ -"""tests/test_branch_sweep_targets.py — the org-wide sweep's target boundary. +"""tests/test_branch_sweep_targets.py — the sweep boundaries this module draws. -The workflow refuses any target this module does not yield, so this is the -line between "a repo whose merged branches get deleted" and "a repo nobody -meant to touch". +Two consumers, two boundaries. `targets()` is the org-wide *branch* sweep's: +the workflow refuses any target it does not yield, so it is the line between "a +repo whose merged branches get deleted" and "a repo nobody meant to touch". +`outside_owner()` is the *settings* sweep's complement: that workflow lists the +organisation from the GitHub API, and this supplies only what such a listing +cannot return — body-map repos under another account. Every fixture here uses invented repo names. That is not squeamishness: this file is `.py` under a framework organ, so the tenant firewall scans it, and an @@ -14,9 +17,12 @@ from __future__ import annotations import importlib.util +import subprocess +import sys from pathlib import Path BRAIN_HOME = Path(__file__).resolve().parents[1] +SCRIPT = str(BRAIN_HOME / "bin" / "branch_sweep_targets.py") _spec = importlib.util.spec_from_file_location( "branch_sweep_targets", BRAIN_HOME / "bin" / "branch_sweep_targets.py" ) @@ -73,68 +79,150 @@ def test_self_sweeping_organs_are_excluded_by_role_not_name(): assert mod.targets(body) == ["Org/TheHeart"] -def test_include_self_sweeping_keeps_the_organs_that_sweep_themselves(): - """The exclusion belongs to the sweep, not to the body map. +# --- outside_owner: the settings sweep's complement ------------------------- - repo_settings.yml flips `delete_branch_on_merge` per repo, and the Mind and - the Brain need it on as much as anything else — two sweepers would contend - over branches, two PATCHes of the same boolean would not. + +def test_outside_owner_yields_only_repos_under_another_account(): + body = _body_map( + { + "Lib": {"github": "Org/Lib", "category": "library"}, + "Mine": {"github": "Person/Mine", "category": "admin"}, + "Ws": {"github": "Org/Ws", "category": "workspace"}, + } + ) + assert mod.outside_owner(body, "Org") == ["Person/Mine"] + + +def test_outside_owner_ignores_categories_on_purpose(): + """The org listing it complements has no category filter, so neither has this. + + Filtering here would drop precisely the repos the flag exists to find: the + categories that end up under a personal account are the ones `targets()` + excludes, so a category-filtered complement would always be empty. """ body = _body_map( { - "TheMind": {"github": "Org/TheMind", "category": "organ", "organ": "Mind"}, - "TheBrain": {"github": "Org/TheBrain", "category": "organ", "organ": "Brain"}, - "TheHeart": {"github": "Org/TheHeart", "category": "organ", "organ": "Heart"}, - "Sci": {"github": "Org/Sci", "category": "assistant"}, + "Sci": {"github": "Person/Sci", "category": "assistant"}, + "Admin": {"github": "Person/Admin", "category": "admin"}, + "Pipe": {"github": "Person/Pipe", "category": "pipeline"}, + "Site": {"github": "Person/Site", "category": "project"}, + "Odd": {"github": "Person/Odd", "category": "some_future_kind"}, + "InOrg": {"github": "Org/InOrg", "category": "library"}, } ) - assert mod.targets(body, include_self_sweeping=True) == [ - "Org/TheMind", - "Org/TheBrain", - "Org/TheHeart", + assert mod.outside_owner(body, "Org") == [ + "Person/Sci", + "Person/Admin", + "Person/Pipe", + "Person/Site", + "Person/Odd", ] - # Widening the organ exclusion must not widen the category boundary. - assert "Org/Sci" not in mod.targets(body, include_self_sweeping=True) -def test_cli_flag_matches_the_function(tmp_path): - import subprocess - import sys +def test_outside_owner_is_empty_when_every_repo_is_in_the_org(): + body = _body_map( + { + "Lib": {"github": "Org/Lib", "category": "library"}, + "Sci": {"github": "Org/Sci", "category": "assistant"}, + } + ) + assert mod.outside_owner(body, "Org") == [] + + +def test_outside_owner_matches_on_the_whole_owner_not_a_prefix(): + """`Org` must not claim `OrgOther/…` — the split is on the slash.""" + body = _body_map( + { + "Near": {"github": "OrgOther/Near", "category": "library"}, + "Ours": {"github": "Org/Ours", "category": "library"}, + } + ) + assert mod.outside_owner(body, "Org") == ["OrgOther/Near"] + + +# --- CLI -------------------------------------------------------------------- + +def _write_body_map(tmp_path, entries): import yaml path = tmp_path / "repos.yaml" - path.write_text( - # sort_keys=False: the output order is body-map order, and an - # alphabetised dump would test the dumper instead of the module. - yaml.safe_dump( - _body_map( - { - "TheMind": {"github": "Org/TheMind", "category": "organ", "organ": "Mind"}, - "Lib": {"github": "Org/Lib", "category": "library"}, - } - ), - sort_keys=False, - ) + # sort_keys=False: the output order is body-map order, and an alphabetised + # dump would test the dumper instead of the module. + path.write_text(yaml.safe_dump(_body_map(entries), sort_keys=False)) + return path + + +def test_cli_matches_the_function(tmp_path): + path = _write_body_map( + tmp_path, + { + "TheMind": {"github": "Org/TheMind", "category": "organ", "organ": "Mind"}, + "Lib": {"github": "Org/Lib", "category": "library"}, + "Mine": {"github": "Person/Mine", "category": "admin"}, + }, ) - script = str(BRAIN_HOME / "bin" / "branch_sweep_targets.py") - plain = subprocess.run([sys.executable, script, str(path)], capture_output=True, text=True) + plain = subprocess.run([sys.executable, SCRIPT, str(path)], capture_output=True, text=True) assert plain.stdout.split() == ["Org/Lib"], plain.stderr - widened = subprocess.run( - [sys.executable, script, "--include-self-sweeping", str(path)], + + complement = subprocess.run( + [sys.executable, SCRIPT, "--outside-owner", "Org", str(path)], capture_output=True, text=True, ) - assert widened.stdout.split() == ["Org/TheMind", "Org/Lib"], widened.stderr + assert complement.stdout.split() == ["Person/Mine"], complement.stderr + + +def test_cli_outside_owner_without_a_value_never_answers(tmp_path): + """A bare flag must not swallow the body-map path as the owner. + + It would otherwise report every repo as "outside " — a wrong answer + delivered with exit 0. Here the path is consumed as the owner, so no path + argument survives and the usage guard fires; either way the contract is + that it fails and prints no slug, not which message it chose. + """ + path = _write_body_map(tmp_path, {"Lib": {"github": "Org/Lib", "category": "library"}}) + proc = subprocess.run( + [sys.executable, SCRIPT, "--outside-owner", str(path)], + capture_output=True, + text=True, + ) + assert proc.returncode == 2 + assert proc.stdout.strip() == "" + assert proc.stderr.strip() + + +def test_cli_outside_owner_at_the_end_names_the_missing_owner(tmp_path): + """Nothing follows the flag at all — the message should say what is missing.""" + proc = subprocess.run( + [sys.executable, SCRIPT, "--outside-owner"], + capture_output=True, + text=True, + ) + assert proc.returncode == 2 + assert "needs an owner" in proc.stderr + + +def test_cli_rejects_the_retired_widening_flag(tmp_path): + """`--include-self-sweeping` is gone; a stale caller must fail, not fall back. + + Reading it as "no flag" would silently sweep the narrow set — the wrong + answer, delivered with exit 0. + """ + path = _write_body_map(tmp_path, {"Lib": {"github": "Org/Lib", "category": "library"}}) + proc = subprocess.run( + [sys.executable, SCRIPT, "--include-self-sweeping", str(path)], + capture_output=True, + text=True, + ) + assert proc.returncode == 2 + assert "usage:" in proc.stderr def test_cli_rejects_an_unknown_flag(tmp_path): """A typo'd flag must not read as "no flag" and silently sweep the wrong set.""" - import subprocess - import sys - proc = subprocess.run( - [sys.executable, str(BRAIN_HOME / "bin" / "branch_sweep_targets.py"), "--include-everything", str(tmp_path)], + [sys.executable, SCRIPT, "--include-everything", str(tmp_path)], capture_output=True, text=True, ) @@ -159,9 +247,6 @@ def test_missing_category_is_excluded(): def test_cli_prints_one_slug_per_line(tmp_path): - import subprocess - import sys - import yaml path = tmp_path / "repos.yaml" @@ -175,22 +260,15 @@ def test_cli_prints_one_slug_per_line(tmp_path): ) ) ) - proc = subprocess.run( - [sys.executable, str(BRAIN_HOME / "bin" / "branch_sweep_targets.py"), str(path)], - capture_output=True, - text=True, - ) + proc = subprocess.run([sys.executable, SCRIPT, str(path)], capture_output=True, text=True) assert proc.returncode == 0, proc.stderr assert proc.stdout.split() == ["Org/Lib"] def test_cli_fails_loudly_on_a_missing_body_map(tmp_path): """Silently sweeping nothing would read as a clean run.""" - import subprocess - import sys - proc = subprocess.run( - [sys.executable, str(BRAIN_HOME / "bin" / "branch_sweep_targets.py"), str(tmp_path / "nope.yaml")], + [sys.executable, SCRIPT, str(tmp_path / "nope.yaml")], capture_output=True, text=True, )