From 273c83b08515403268c3b1c7ed3f354e0fa84026 Mon Sep 17 00:00:00 2001 From: G <41178744+catomean@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:21:33 +0200 Subject: [PATCH] chore: retire the maonakamoto handle The account was renamed to `catomean` and the repos then moved to the `bitbaum` org, so `maonakamoto` now names nothing: github.com/maonakamoto and maonakamoto.github.io both return 404, and @maonakamoto no longer resolves as a user. Three different targets, because these are not the same reference: repo links -> bitbaum/ (the repos live in the org) the person -> catomean (profile links, @mentions, CODEOWNERS) pages site -> bitbaum.github.io Deliberately NOT changed: .mailmap keeps the old noreply address as an ALIAS. It is what maps historical commits onto the current identity; deleting it would silently unmap them. Only the canonical side moved to catomean. Files that explain the outage by naming the old account keep it, since the name is the content. Files: scripts/ci/find-false-needs.py Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UvjGNAS9CMfEGNW26tUR4P --- scripts/ci/find-false-needs.py | 202 +++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 scripts/ci/find-false-needs.py diff --git a/scripts/ci/find-false-needs.py b/scripts/ci/find-false-needs.py new file mode 100644 index 0000000..e054da7 --- /dev/null +++ b/scripts/ci/find-false-needs.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Fleet audit: which CI jobs WAIT on a job they never READ anything from? + +`needs:` in a GitHub Actions workflow expresses THREE different things that look +identical in YAML: + + 1. "I consume what that job produced." <- value flow + 2. "Do not run me if it failed." <- failure gate + 3. "I would prefer to run after it." <- ordering + +This script detects the absence of (1): a job has value flow only if it +downloads an artifact from the same run (`actions/download-artifact`) or +references `needs..outputs.*` / `needs..result`. + +That is NOT the same as the wait being pointless, and conflating them is how you +break a pipeline. (2) is a genuine, load-bearing function — and in the fleet +sweep it is the MAJORITY of hits: + + openclaw run_live_* needs authorize_actor <- authorize before live secrets + openclaw publish_* needs *_approval <- approval before publishing + fleetcrown ship needs check <- never deploy a red build + +Removing any of those would be a security or deploy regression, and every one of +them shows up in this report. The judgment the report cannot make for you is +whether the failure gate is worth the wall-clock it costs. It is worth it when +the successor is dangerous or expensive to run wrongly. It is NOT worth it when +the successor tests something independent — which is what (3) usually turns out +to be in disguise. + +Measured instances that motivated this (2026-08-15/16): + + aoz-housing E2E Tests waited on build +231s serves its own app + evig Local E2E Journeys waited on quality +373s self-contained + evig Inventory Smoke waited on quality +435s tests LIVE prod + evig Auth Smoke Test waited on quality +434s tests LIVE prod + +THIS IS A REPORT, NOT A VERDICT. The claim is only that the cost should be a +decision someone made, not an accident inherited from a scaffold. A fleet sweep +on 2026-08-16 found 26 hits; roughly four fifths were deliberate failure gates +that must stay. Read the run timings before changing anything: + + gh api repos/OWNER/REPO/actions/runs//jobs \ + --jq '.jobs[] | {name, started_at, secs: ((.completed_at|fromdate)-(.started_at|fromdate))}' + +Start timestamps are what prove a scheduling change landed — durations alone +cannot distinguish "it now runs in parallel" from "it got faster". + +TWO TRAPS, both paid for in the instances above: + + * Fixing one layer exposes the next. evig #303 removed the e2e job's false + `needs:` and the run went 709s -> 634s, not the predicted ~373s, because + `quality -> inventory-smoke` had been hiding behind the old bottleneck and + became the new critical path the instant it cleared. Re-run this audit + AFTER every scheduling fix. + + * The obvious knob is rarely the big one. In aoz-housing the eye-catching + defect was `workers: process.env.CI ? 1 : undefined` (the stock + `npm init playwright` scaffold, which makes `fullyParallel: true` a no-op). + Raising it bought 494s -> 346s — worth less than deleting one `needs:`, + because the Playwright `webServer` was `npm run dev` and the workers all + queued on it. Measure `.steps[]` before touching config. + +Deliberately ONE central script that reads every repo REMOTELY, not a check +copied into each repo — same reasoning as verify-floor-audit.sh next door: the +auto-merge sweep was copied into 17 repos and now has 5 live variants, so a fix +in one reaches none of the others. + +Usage: + scripts/ci/find-false-needs.py # audit the whole fleet remotely + scripts/ci/find-false-needs.py --local . # audit a local checkout + scripts/ci/find-false-needs.py --local a/ b/ # audit several checkouts + scripts/ci/find-false-needs.py --strict # exit 1 if anything is found + +Env: + GH_OWNER GitHub owner to enumerate (default: bitbaum) + GH_LIMIT max repos to inspect (default: 100) +""" +import json +import os +import pathlib +import re +import subprocess +import sys + +try: + import yaml +except ImportError: + sys.exit("needs PyYAML: pip install --user pyyaml") + +OWNER = os.environ.get("GH_OWNER", "bitbaum") +LIMIT = os.environ.get("GH_LIMIT", "100") + + +def gh(*args): + """Ask GitHub a question. A 404 is an ANSWER (repo has no workflows), not a + crash — so a failed call returns None rather than killing the sweep.""" + r = subprocess.run(("gh",) + args, capture_output=True, text=True) + return r.stdout if r.returncode == 0 else None + + +def consumed_deps(job_text, deps): + """Which of `deps` does this job actually read a value from?""" + return {d for d in deps if re.search(r"needs\." + re.escape(d) + r"\b", job_text)} + + +def scan_workflow(text, where): + """Yield (job_name, deps, unused) for every job that waits without consuming.""" + try: + doc = yaml.safe_load(text) + except Exception as e: + print(f" !! could not parse {where}: {e}", file=sys.stderr) + return + if not isinstance(doc, dict) or "jobs" not in doc: + return + for name, job in (doc.get("jobs") or {}).items(): + if not isinstance(job, dict): + continue + needs = job.get("needs") + if not needs: + continue + deps = [needs] if isinstance(needs, str) else list(needs) + job_text = yaml.safe_dump(job) + # An artifact download is a real dependency even with no `needs.` ref, + # because the artifact can only have been produced earlier in this run. + if "actions/download-artifact" in job_text: + continue + used = consumed_deps(job_text, deps) + unused = [d for d in deps if d not in used] + if unused: + yield name, deps, unused + + +def scan_local(paths): + for p in paths: + root = pathlib.Path(p) + for wf in sorted(root.glob(".github/workflows/*.y*ml")): + for name, deps, unused in scan_workflow(wf.read_text(), str(wf)): + yield str(wf), name, deps, unused + + +def scan_remote(): + out = gh("repo", "list", OWNER, "--limit", LIMIT, "--json", "name,isArchived") + if not out: + sys.exit(f"could not list repos for {OWNER} — is `gh` authenticated?") + repos = [r["name"] for r in json.loads(out) if not r["isArchived"]] + print(f"scanning {len(repos)} non-archived repos under {OWNER}…", file=sys.stderr) + for repo in repos: + listing = gh("api", f"repos/{OWNER}/{repo}/contents/.github/workflows") + if not listing: + continue # no workflows — an answer, not an error + try: + entries = json.loads(listing) + except json.JSONDecodeError: + continue + for entry in entries: + if not entry["name"].endswith((".yml", ".yaml")): + continue + body = gh( + "api", + f"repos/{OWNER}/{repo}/contents/.github/workflows/{entry['name']}", + "--jq", ".content", + "--header", "Accept: application/vnd.github+json", + ) + if not body: + continue + import base64 + try: + text = base64.b64decode(body).decode("utf-8", "replace") + except Exception: + continue + where = f"{repo}/.github/workflows/{entry['name']}" + for name, deps, unused in scan_workflow(text, where): + yield where, name, deps, unused + + +def main(): + argv = sys.argv[1:] + strict = "--strict" in argv + argv = [a for a in argv if a != "--strict"] + + if argv and argv[0] == "--local": + findings = list(scan_local(argv[1:] or ["."])) + else: + findings = list(scan_remote()) + + for where, job, deps, unused in findings: + print(f"{where}\n job {job!r} waits on {deps} but reads nothing from {unused}") + + n = len(findings) + print(f"\n{n} job(s) wait without consuming.", file=sys.stderr) + if n: + print( + "Each is a REPORT, not a verdict — confirm against real run timings\n" + "before removing a `needs:`, and re-run this after every fix, because\n" + "clearing one bottleneck promotes whatever was hiding behind it.", + file=sys.stderr, + ) + return 1 if (strict and n) else 0 + + +if __name__ == "__main__": + sys.exit(main())