96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Fetch owner source for the bounded-execution contract tests (NK-WP-0058-T02). Read-only, no credentials.
|
||
|
|
|
||
|
|
python3 tools/contract-tests/fetch_owners.py --dest DIR [--ref pinned|main] # CI: owners next to net-kingdom
|
||
|
|
python3 tools/contract-tests/fetch_owners.py --drift # local: pins vs sibling checkouts
|
||
|
|
python3 tools/contract-tests/fetch_owners.py --bump # local: pin the siblings' HEADs
|
||
|
|
|
||
|
|
`contracts/owner-pins.json` names each owner's commit. CI on push tests against the pins, so an owner change
|
||
|
|
reaches net-kingdom when a pin moves; the nightly run tests against each owner's main, so a breaking owner change
|
||
|
|
fails within a day. Owners are anonymous partial clones of public Forgejo repositories (no credentials, no
|
||
|
|
prompt). --bump refuses a sibling with uncommitted or unpushed work.
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
REPO = Path(__file__).resolve().parents[2]
|
||
|
|
PINS = REPO / 'contracts/owner-pins.json'
|
||
|
|
|
||
|
|
|
||
|
|
def load():
|
||
|
|
"""The pins as {repo: commit}. The file lists objects (repo, commit) rather than repo-named keys: a key like
|
||
|
|
'key-cape' or 'access-engine' next to a hex value reads as a credential to the commit-time secret scanner."""
|
||
|
|
doc = json.loads(PINS.read_text())
|
||
|
|
return {**doc, 'owners': {o['repo']: o['commit'] for o in doc['owners']}}
|
||
|
|
|
||
|
|
|
||
|
|
def fetch(dest, ref='pinned', run=subprocess.run):
|
||
|
|
"""A partial clone of each owner (no blobs until needed), checked out at the pin or at `ref`. History stays
|
||
|
|
available: the readiness check reads informed-decision at the revision Argo pins."""
|
||
|
|
pins = load()
|
||
|
|
dest.mkdir(parents=True, exist_ok=True)
|
||
|
|
fetched = {}
|
||
|
|
env = {'GIT_TERMINAL_PROMPT': '0', 'PATH': '/usr/local/bin:/usr/bin:/bin'}
|
||
|
|
for owner, commit in pins['owners'].items():
|
||
|
|
target = dest / owner
|
||
|
|
rev = commit if ref == 'pinned' else 'origin/' + ref
|
||
|
|
for argv in (['git', 'clone', '--quiet', '--filter=blob:none', '--no-checkout',
|
||
|
|
f"{pins['base_url']}/{owner}.git", str(target)],
|
||
|
|
['git', '-C', str(target), 'checkout', '--quiet', '--detach', rev]):
|
||
|
|
if run(argv, capture_output=True, env=env).returncode:
|
||
|
|
raise SystemExit(f'{owner}: {argv[1] if argv[1] != "-C" else "checkout"} failed')
|
||
|
|
fetched[owner] = run(['git', '-C', str(target), 'rev-parse', 'HEAD'], capture_output=True,
|
||
|
|
text=True).stdout.strip()
|
||
|
|
return fetched
|
||
|
|
|
||
|
|
|
||
|
|
def git(path, *args):
|
||
|
|
return subprocess.run(['git', '-C', str(path), *args], capture_output=True, text=True)
|
||
|
|
|
||
|
|
|
||
|
|
def drift(siblings=REPO.parent):
|
||
|
|
out = {}
|
||
|
|
for owner, commit in load()['owners'].items():
|
||
|
|
head = git(siblings / owner, 'rev-parse', 'HEAD').stdout.strip()
|
||
|
|
out[owner] = 'pinned' if head == commit else ('absent' if not head else f'sibling_at_{head[:12]}')
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def bump(siblings=REPO.parent):
|
||
|
|
pins = load()
|
||
|
|
for owner in pins['owners']:
|
||
|
|
path = siblings / owner
|
||
|
|
if git(path, 'status', '--porcelain').stdout.strip():
|
||
|
|
raise SystemExit(f'{owner}: uncommitted work; not pinned')
|
||
|
|
if git(path, 'rev-list', '--count', '@{u}..HEAD').stdout.strip() not in ('0', ''):
|
||
|
|
raise SystemExit(f'{owner}: unpushed commits; not pinned')
|
||
|
|
pins['owners'][owner] = git(path, 'rev-parse', 'HEAD').stdout.strip()
|
||
|
|
doc = {**pins, 'owners': [{'repo': o, 'commit': c} for o, c in pins['owners'].items()]}
|
||
|
|
PINS.write_text(json.dumps(doc, indent=2) + '\n')
|
||
|
|
return {owner: commit[:12] for owner, commit in pins['owners'].items()}
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv=None):
|
||
|
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
|
|
p.add_argument('--dest', type=Path)
|
||
|
|
p.add_argument('--ref', default='pinned', help="'pinned' (default) or a branch name, e.g. main")
|
||
|
|
p.add_argument('--drift', action='store_true')
|
||
|
|
p.add_argument('--bump', action='store_true')
|
||
|
|
a = p.parse_args(argv)
|
||
|
|
if a.drift:
|
||
|
|
print(json.dumps(drift()))
|
||
|
|
elif a.bump:
|
||
|
|
print(json.dumps(bump()))
|
||
|
|
elif a.dest:
|
||
|
|
print(json.dumps(fetch(a.dest, a.ref)))
|
||
|
|
else:
|
||
|
|
p.error('give --dest, --drift or --bump')
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
sys.exit(main())
|