mirror of
https://github.com/piabrota/mu.git
synced 2026-11-07 06:15:59 -10:00
* The repo scripts make repos on an operator's own account ("owner": "user")
Any operator can now make private or public repos on their own GitHub account with the same setup as
ouroboric-dev's. A config with "owner": "user" gets it, with the differences GitHub forces:
- create-repo.sh creates through /user/repos, after checking that the token belongs to the config's
user, so a repo never lands on the wrong account.
- setup-repo.sh writes each user repo's own Actions allowlist (lint.yml's pinned actions, nothing
else), since a user has no org to hold it. On a user's private repo it skips secret scanning, push
protection and private vulnerability reporting, which GitHub doesn't offer there, and says so.
- If GitHub refuses the ruleset (a user's private repo without GitHub Pro), setup-repo.sh fails and
says main is not protected, after every other setting is in place; new-repo.sh then reports the repo
as unprotected. The ruleset itself is unchanged.
An operator's config is theirs, not the company's: it lives in scripts/orgs/local/, which git ignores
except for its README, and --org reads it from there (a name in both folders is refused). Only
generic examples are committed, in scripts/orgs/examples/: a user config and a proprietary licence
text that names no one. Invariant 9 in AGENTS.md says never to commit an operator's config, and a test
fails if any committed config is neither an org's nor an example. The fakes learn GET /user,
POST /user/repos and refused writes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* check-repos.sh --check operator: no tracked file names the operator (invariant 9)
The check reads each repo's local clone and fails when a tracked file names the operator who runs it: their
login, their home folder's name, or a user account in scripts/orgs/local/, as whole words in any case. The
names come from the machine, so the check serves any operator and the script names no one;
CHECK_REPOS_OPERATOR replaces them. It prints the files, never the names, and fails closed on a missing
clone, an empty name list or a git error. The records of past work keep their paths as written: RECORDS
lists those files one by one, so a new plan beside them is still checked. It is a default check.
The local-qwen design, plan and how-to now give paths relative to the clones, and the design no longer
names the founder's folders. Invariant 9 also covers docs, plans and fixtures.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Bruno Guilhermo de Barros Valério <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
73 lines
3.1 KiB
Python
73 lines
3.1 KiB
Python
# Fake GitHub API for the script tests. Logs each request to LOG as one JSON line.
|
|
# python3 -I fake-github.py LOG PORTFILE
|
|
# FAKE_PEM: the key the manifest conversion returns. FAKE_GIT_ROOT: where a created repo gets its
|
|
# bare git repo, so a test can push to file://$FAKE_GIT_ROOT/<org>/<repo>.git. FAKE_USER: the login
|
|
# that GET /user returns, and the account that POST /user/repos creates in (default acme).
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
log, portfile = sys.argv[1:3]
|
|
repos = set() # (org, name) of the repos created; "taken" exists in every org
|
|
|
|
|
|
def repo_routes(method, path, body):
|
|
if method == "GET" and (m := re.fullmatch(r"/repos/([^/]+)/([^/]+)", path)):
|
|
org, name = m.groups()
|
|
return (200, {"name": name}) if name == "taken" or (org, name) in repos else None
|
|
if method == "GET" and path == "/user":
|
|
return 200, {"login": os.environ.get("FAKE_USER", "acme")}
|
|
if method == "POST" and (m := re.fullmatch(r"/orgs/([^/]+)/repos|/user/repos", path)):
|
|
org = m.group(1) or os.environ.get("FAKE_USER", "acme")
|
|
name = json.loads(body)["name"]
|
|
if name == "taken" or (org, name) in repos:
|
|
return 422, {"message": "Repository creation failed."}
|
|
repos.add((org, name))
|
|
subprocess.run(["git", "init", "-q", "--bare", "-b", "main",
|
|
os.path.join(os.environ["FAKE_GIT_ROOT"], org, name + ".git")], check=True)
|
|
return 201, {"name": name}
|
|
return None
|
|
|
|
|
|
def routes():
|
|
return {
|
|
("GET", "/orgs/ouroboric-dev/installation"): (200, {"id": 42}),
|
|
("POST", "/app/installations/42/access_tokens"):
|
|
(201, {"token": "ghs_FAKE", "expires_at": "2099-01-01T00:00:00Z"}),
|
|
("POST", "/app-manifests/good-code/conversions"):
|
|
(201, {"id": 7, "slug": "ouroboric-pr-bot", "client_id": "Iv1.test",
|
|
"client_secret": "SECRET_CS", "webhook_secret": None,
|
|
"pem": open(os.environ["FAKE_PEM"]).read() if "FAKE_PEM" in os.environ else ""}),
|
|
}
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
def handle_any(self):
|
|
n = int(self.headers.get("Content-Length") or 0)
|
|
body = self.rfile.read(n).decode() if n else ""
|
|
with open(log, "a") as f:
|
|
f.write(json.dumps({"method": self.command, "path": self.path,
|
|
"auth": self.headers.get("Authorization", ""), "body": body}) + "\n")
|
|
code, obj = repo_routes(self.command, self.path, body) \
|
|
or routes().get((self.command, self.path), (404, {"message": "Not Found"}))
|
|
data = json.dumps(obj).encode()
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
|
|
do_GET = do_POST = handle_any
|
|
|
|
|
|
server = HTTPServer(("127.0.0.1", 0), Handler)
|
|
with open(portfile, "w") as f:
|
|
f.write(str(server.server_address[1]))
|
|
server.serve_forever()
|