v1_mirror.py
python
sha256:c87cf2c2f03da507f66f93922cb339a2cbebb33ad6372a6039032fb09a20da12
Reconcile reviewed Overseer recovery d47291d5d9030de5ebef71…
Human
minor
⚠ breaking
11 days ago
| 1 | """Controlled snapshot mirrors. Preparation is offline; delivery is explicit. |
| 2 | |
| 3 | One source checkout, source main branch and Git destination pair. Only a newly |
| 4 | created, dedicated bare Git target or this engine's own target can be used. |
| 5 | Git plumbing avoids checkout filters, index state, source hooks and native export |
| 6 | side effects. A single correspondence record plus commit trailers supports retry. |
| 7 | """ |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import base64 |
| 11 | from contextlib import contextmanager |
| 12 | import fcntl |
| 13 | import json |
| 14 | import os |
| 15 | from pathlib import Path |
| 16 | import re |
| 17 | import stat |
| 18 | import subprocess |
| 19 | import tempfile |
| 20 | import urllib.request |
| 21 | |
| 22 | from cli.v1_io import Refusal, atomic, digest, read |
| 23 | from cli.v1_revision import OID, bounded_run, revision, unique_json |
| 24 | |
| 25 | RECORD = '.overseer/local/git-bridge.json' |
| 26 | MARKER = 'overseer-mirror.json' |
| 27 | HEX = re.compile(r'[0-9a-f]{40}\Z') |
| 28 | CONFIG = b'[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n\thooksPath = /dev/null\n\tlogAllRefUpdates = false\n' |
| 29 | |
| 30 | |
| 31 | def packed(value): |
| 32 | return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode() |
| 33 | |
| 34 | |
| 35 | def env(): |
| 36 | result = {k: v for k, v in os.environ.items() |
| 37 | if not k.startswith(('GIT_', 'MUSE_', 'PYTHON')) and k != 'VIRTUAL_ENV'} |
| 38 | result.update(PATH='/usr/bin:/bin', GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', |
| 39 | GIT_TERMINAL_PROMPT='0', GIT_OPTIONAL_LOCKS='0', LC_ALL='C', |
| 40 | GH_HOST='github.com', GH_PROMPT_DISABLED='1', |
| 41 | GIT_AUTHOR_NAME='Overseer mirror', GIT_AUTHOR_EMAIL='[email protected]', |
| 42 | GIT_COMMITTER_NAME='Overseer mirror', GIT_COMMITTER_EMAIL='[email protected]') |
| 43 | return result |
| 44 | |
| 45 | |
| 46 | def git(target, *args, data=None, optional=False): |
| 47 | proc = subprocess.run(['/usr/bin/git', '--no-replace-objects', '-c', 'core.hooksPath=/dev/null', |
| 48 | '-c', 'core.fsmonitor=false', '-c', 'gc.auto=0', '-C', str(target), *args], |
| 49 | input=data, env=env(), capture_output=True, timeout=30) |
| 50 | if proc.returncode and not optional: |
| 51 | raise Refusal('mirror_git_failed: ' + args[0]) |
| 52 | return proc.stdout if not proc.returncode else b'' |
| 53 | |
| 54 | |
| 55 | def head(target): |
| 56 | return git(target, 'rev-parse', '--verify', 'HEAD', optional=True).decode().strip() or 'absent' |
| 57 | |
| 58 | |
| 59 | def physical(value): |
| 60 | if not isinstance(value, str): |
| 61 | raise Refusal('mirror_path_requires_physical_absolute_root') |
| 62 | path = Path(value) |
| 63 | if not path.is_absolute() or path.resolve() != path or any(p.is_symlink() for p in (path, *path.parents)): |
| 64 | raise Refusal('mirror_path_requires_physical_absolute_root') |
| 65 | return path |
| 66 | |
| 67 | |
| 68 | def validate_plan(plan, root, config): |
| 69 | from cli.v1 import keys, KIT |
| 70 | keys(plan, ('schema', 'source', 'destination', 'target', 'expected_target', 'executables')) |
| 71 | keys(plan['source'], ('repo_id', 'branch', 'revision', 'hub_url')) |
| 72 | keys(plan['destination'], ('url', 'repository', 'branch', 'base', 'expected_head')) |
| 73 | source, dest = plan['source'], plan['destination'] |
| 74 | if plan['schema'] != 1 or config['vcs'] != 'muse': |
| 75 | raise Refusal('mirror_requires_explicit_muse_authority') |
| 76 | if source['branch'] != 'main' or not OID.fullmatch(source['revision']): |
| 77 | raise Refusal('mirror_requires_approved_main_revision') |
| 78 | if source['repo_id'] != config['muse']['repo_id']: |
| 79 | raise Refusal('mirror_source_identity_mismatch') |
| 80 | # Public staging is the tested authority. Do not bypass unresolved production trust. |
| 81 | if not re.fullmatch(r'https://staging\.musehub\.ai/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', source['hub_url']): |
| 82 | raise Refusal('mirror_authority_url_unsupported') |
| 83 | if not re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', dest['repository']): |
| 84 | raise Refusal('mirror_github_repository_invalid') |
| 85 | if dest['url'] != 'https://github.com/' + dest['repository'] + '.git': |
| 86 | raise Refusal('mirror_destination_repository_mismatch') |
| 87 | for branch in (dest['branch'], dest['base']): |
| 88 | if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_/-]*', branch) or branch.endswith('/') or '//' in branch: |
| 89 | raise Refusal('mirror_git_branch_invalid') |
| 90 | if dest['branch'] in {'main', 'master', dest['base']}: |
| 91 | raise Refusal('mirror_requires_distribution_branch') |
| 92 | for expected in (plan['expected_target'], dest['expected_head']): |
| 93 | if expected != 'absent' and not HEX.fullmatch(expected): |
| 94 | raise Refusal('mirror_expected_head_invalid') |
| 95 | if plan['expected_target'] == 'absent' and dest['expected_head'] != 'absent': |
| 96 | raise Refusal('mirror_legacy_destination_requires_reconciliation') |
| 97 | ex = plan['executables'] |
| 98 | if not isinstance(ex, list) or any(not isinstance(p, str) for p in ex) or ex != sorted(set(ex)): |
| 99 | raise Refusal('mirror_executable_policy_invalid') |
| 100 | target = physical(plan['target']) |
| 101 | for protected in (root, Path(config['muse']['store']), KIT): |
| 102 | if target.is_relative_to(protected) or protected.is_relative_to(target): |
| 103 | raise Refusal('mirror_target_overlap') |
| 104 | if not target.parent.is_dir(): |
| 105 | raise Refusal('mirror_target_parent_missing') |
| 106 | return target |
| 107 | |
| 108 | |
| 109 | def binding(plan, config): |
| 110 | return {'source_root': config['repo']['root'], 'checkout_id': config['repo']['id'], |
| 111 | 'source_id': plan['source']['repo_id'], 'source_branch': plan['source']['branch'], |
| 112 | 'hub_url': plan['source']['hub_url'], 'target': plan['target'], |
| 113 | 'destination': {k: v for k, v in plan['destination'].items() if k != 'expected_head'}} |
| 114 | |
| 115 | |
| 116 | @contextmanager |
| 117 | def locked(root, target): |
| 118 | handles = [] |
| 119 | try: |
| 120 | # Lock before target creation. Physical aliases share the same directory inode. |
| 121 | for path in (root, target.parent): |
| 122 | fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) |
| 123 | handles.append(fd) |
| 124 | try: |
| 125 | fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 126 | except BlockingIOError: |
| 127 | raise Refusal('mirror_operator_busy') from None |
| 128 | yield |
| 129 | finally: |
| 130 | for fd in handles: |
| 131 | os.close(fd) |
| 132 | |
| 133 | |
| 134 | def check_source(root, config, plan): |
| 135 | state = revision(root, config) |
| 136 | if (state.branch, state.head, state.source_id) != ( |
| 137 | plan['source']['branch'], plan['source']['revision'], plan['source']['repo_id']): |
| 138 | raise Refusal('mirror_source_drift') |
| 139 | python = config['muse']['runtime']['python'] |
| 140 | helper = Path(__file__).with_name('v1_mirror_reader.py') |
| 141 | proc = bounded_run([python, '-I', '-B', str(helper), str(root), json.dumps(plan['executables'])], |
| 142 | cwd=root, env=env(), limit=48 * 1024 * 1024, timeout=30) |
| 143 | if proc.returncode: |
| 144 | try: |
| 145 | reason = json.loads(proc.stdout, object_pairs_hook=unique_json)['error'] |
| 146 | if not isinstance(reason, str) or len(reason) > 300: |
| 147 | raise ValueError() |
| 148 | except (ValueError, KeyError, TypeError): |
| 149 | reason = 'invalid reader response' |
| 150 | raise Refusal('mirror_projection_refused: ' + reason) |
| 151 | value = json.loads(proc.stdout, object_pairs_hook=unique_json) |
| 152 | if (value['state']['runtime'] != config['muse']['runtime'] |
| 153 | or value['state']['head'] != state.head or value['state']['repo_id'] != state.source_id |
| 154 | or value['state']['branch'] != state.branch or value['state']['root'] != str(root) |
| 155 | or value['state']['store'] != config['muse']['store']): |
| 156 | raise Refusal('mirror_source_changed') |
| 157 | return value |
| 158 | |
| 159 | |
| 160 | def inspect_target(target, pair): |
| 161 | physical(str(target)) |
| 162 | if not target.exists(): |
| 163 | return 'absent' |
| 164 | if not target.is_dir() or not list(target.iterdir()): |
| 165 | raise Refusal('mirror_target_not_owned') |
| 166 | if read(target, MARKER, missing=True) != packed(pair): |
| 167 | raise Refusal('mirror_foreign_target') |
| 168 | if read(target, 'config') != CONFIG: |
| 169 | raise Refusal('mirror_target_config_or_hooks_changed') |
| 170 | allowed = {'HEAD', 'config', MARKER, 'objects', 'refs'} |
| 171 | if set(p.name for p in target.iterdir()) - allowed: |
| 172 | raise Refusal('mirror_dirty_or_hooked_target') |
| 173 | for path in target.rglob('*'): |
| 174 | mode = path.lstat() |
| 175 | if stat.S_ISLNK(mode.st_mode) or (not stat.S_ISDIR(mode.st_mode) and |
| 176 | (not stat.S_ISREG(mode.st_mode) or mode.st_nlink != 1)): |
| 177 | raise Refusal('mirror_target_unsafe_path') |
| 178 | rel = path.relative_to(target).as_posix() |
| 179 | if path.is_dir() and rel not in {'objects', 'refs', 'refs/heads'}: |
| 180 | ref_dir = 'refs/heads/' + pair['destination']['branch'] |
| 181 | if not re.fullmatch(r'objects/[0-9a-f]{2}', rel) and not ref_dir.startswith(rel + '/'): |
| 182 | raise Refusal('mirror_foreign_directory') |
| 183 | if path.is_file() and rel.startswith('refs/') and rel != 'refs/heads/' + pair['destination']['branch']: |
| 184 | raise Refusal('mirror_foreign_ref_file') |
| 185 | if path.is_file() and rel.startswith('objects/') and not re.fullmatch(r'objects/[0-9a-f]{2}/[0-9a-f]{38}', rel): |
| 186 | raise Refusal('mirror_foreign_object_or_alternate') |
| 187 | branch = pair['destination']['branch'] |
| 188 | if read(target, 'HEAD') != ('ref: refs/heads/' + branch + '\n').encode(): |
| 189 | raise Refusal('mirror_target_branch_changed') |
| 190 | refs = git(target, 'for-each-ref', '--format=%(refname)').decode().splitlines() |
| 191 | if set(refs) - {'refs/heads/' + branch}: |
| 192 | raise Refusal('mirror_foreign_refs') |
| 193 | return head(target) |
| 194 | |
| 195 | |
| 196 | def create_target(target, pair): |
| 197 | # Initialize a sibling first: a killed init never leaves an ambiguous target. |
| 198 | with tempfile.TemporaryDirectory(prefix='.overseer-mirror-', dir=target.parent) as name: |
| 199 | stage = Path(name) |
| 200 | git(stage, 'init', '--bare', '--template=', '--initial-branch=' + pair['destination']['branch']) |
| 201 | (stage / 'config').write_bytes(CONFIG) |
| 202 | # Git init's empty metadata directories are unnecessary for loose objects. |
| 203 | (stage / 'objects/info').rmdir() |
| 204 | (stage / 'objects/pack').rmdir() |
| 205 | (stage / 'refs/tags').rmdir() |
| 206 | (stage / MARKER).write_bytes(packed(pair)) |
| 207 | if target.exists(): |
| 208 | raise Refusal('mirror_target_appeared') |
| 209 | os.rename(stage, target) |
| 210 | |
| 211 | |
| 212 | def tree(target, files): |
| 213 | entries = {} |
| 214 | for path, item in files.items(): |
| 215 | node = entries |
| 216 | parts = path.split('/') |
| 217 | for part in parts[:-1]: |
| 218 | node = node.setdefault(part, {}) |
| 219 | data = base64.b64decode(item['data'], validate=True) |
| 220 | oid = git(target, 'hash-object', '-w', '--stdin', data=data).decode().strip() |
| 221 | node[parts[-1]] = (item['mode'], oid) |
| 222 | def write(node): |
| 223 | rows = [] |
| 224 | for name, item in sorted(node.items()): |
| 225 | mode, oid, kind = ('040000', write(item), 'tree') if isinstance(item, dict) else (*item, 'blob') |
| 226 | rows.append(f'{mode} {kind} {oid}\t{name}'.encode() + b'\0') |
| 227 | return git(target, 'mktree', '-z', data=b''.join(rows)).decode().strip() |
| 228 | return write(entries) |
| 229 | |
| 230 | |
| 231 | def verify_tree(target, commit, files): |
| 232 | found = {} |
| 233 | for row in git(target, 'ls-tree', '-r', '-z', commit).split(b'\0'): |
| 234 | if not row: |
| 235 | continue |
| 236 | meta, path = row.split(b'\t', 1) |
| 237 | mode, kind, oid = meta.decode().split() |
| 238 | path = path.decode() |
| 239 | if kind != 'blob' or path not in files or mode != files[path]['mode']: |
| 240 | raise Refusal('mirror_tree_paths_or_modes_mismatch') |
| 241 | content = git(target, 'cat-file', 'blob', oid) |
| 242 | if content != base64.b64decode(files[path]['data'], validate=True): |
| 243 | raise Refusal('mirror_tree_bytes_mismatch') |
| 244 | found[path] = True |
| 245 | if set(found) != set(files): |
| 246 | raise Refusal('mirror_tree_paths_mismatch') |
| 247 | |
| 248 | |
| 249 | def correspondence(target, commit, plan, plan_hash, files): |
| 250 | if commit == 'absent': |
| 251 | return False |
| 252 | message = git(target, 'show', '-s', '--format=%B', commit).decode() |
| 253 | trailers = f'Muse-Source: {plan["source"]["revision"]}\nOverseer-Mirror-Plan: {plan_hash}\n' |
| 254 | if not message.endswith(trailers + '\n'): |
| 255 | return False |
| 256 | parents = git(target, 'show', '-s', '--format=%P', commit).decode().strip() |
| 257 | if parents != ('' if plan['expected_target'] == 'absent' else plan['expected_target']): |
| 258 | raise Refusal('mirror_commit_parent_mismatch') |
| 259 | verify_tree(target, commit, files) |
| 260 | return True |
| 261 | |
| 262 | |
| 263 | def save_record(root, old, pair, plan, plan_hash, commit): |
| 264 | record = {'binding': pair, 'last_export': {'muse_branch': plan['source']['branch'], |
| 265 | 'muse_commit_id': plan['source']['revision'], 'git_remote': plan['destination']['url'], |
| 266 | 'git_ref': plan['destination']['branch'], 'git_sha': commit, 'plan_sha256': plan_hash}} |
| 267 | data = packed(record) |
| 268 | if data != old: |
| 269 | atomic(root, RECORD, data, expected='absent' if old is None else digest(old)) |
| 270 | |
| 271 | |
| 272 | def execute(root, config, original, plan, plan_hash, operation, transport=None): |
| 273 | target = validate_plan(plan, root, config) |
| 274 | result = {'ok': False, 'repository': config['repo'], 'source': plan['source'], |
| 275 | 'target': str(target), 'destination': plan['destination'], 'plan_sha256': plan_hash, |
| 276 | 'export': 'not_prepared', 'push': 'not_attempted', 'pr': 'not_attempted', |
| 277 | 'authority': 'approved_input_not_checked_offline'} |
| 278 | with locked(root, target): |
| 279 | try: |
| 280 | pair = binding(plan, config) |
| 281 | old = read(root, RECORD, missing=True) |
| 282 | if old and json.loads(old)['binding'] != pair: |
| 283 | raise Refusal('mirror_pair_changed_requires_reconciliation') |
| 284 | observed = inspect_target(target, pair) |
| 285 | projection = check_source(root, config, plan) |
| 286 | files = projection['files'] |
| 287 | result.update(excluded=projection['excluded'], files=len(files), |
| 288 | directories_omitted=projection['directories_omitted']) |
| 289 | already = observed != 'absent' and correspondence(target, observed, plan, plan_hash, files) |
| 290 | if not already and observed != plan['expected_target']: |
| 291 | raise Refusal('mirror_target_head_drift') |
| 292 | if not already and operation != 'prepare': |
| 293 | raise Refusal('mirror_preparation_required') |
| 294 | if not already: |
| 295 | if observed == 'absent' and target.exists(): |
| 296 | # Only an owned, unborn target left by an interrupted prepare. |
| 297 | inspect_target(target, pair) |
| 298 | elif observed == 'absent': |
| 299 | create_target(target, pair) |
| 300 | else: |
| 301 | if not old or json.loads(old)['last_export']['git_sha'] != observed: |
| 302 | raise Refusal('mirror_target_mapping_mismatch') |
| 303 | # Refuse legacy/foreign content instead of sweeping it away. |
| 304 | previous_plan = json.loads(old)['last_export']['plan_sha256'] |
| 305 | message = git(target, 'show', '-s', '--format=%B', observed).decode() |
| 306 | if f'Overseer-Mirror-Plan: {previous_plan}\n' not in message: |
| 307 | raise Refusal('mirror_target_correspondence_missing') |
| 308 | new_tree = tree(target, files) |
| 309 | same_tree = observed != 'absent' and git(target, 'rev-parse', observed + '^{tree}').decode().strip() == new_tree |
| 310 | message = f'Mirror approved Muse snapshot\n\nMuse-Source: {plan["source"]["revision"]}\nOverseer-Mirror-Plan: {plan_hash}\n' |
| 311 | parents = [] if observed == 'absent' else ['-p', observed] |
| 312 | commit = git(target, 'commit-tree', new_tree, *parents, data=message.encode()).decode().strip() |
| 313 | verify_tree(target, commit, files) |
| 314 | # Recheck immutable source, working projection, config and target before the CAS. |
| 315 | if check_source(root, config, plan) != projection or read(root, '.overseer/config.yaml') != original: |
| 316 | raise Refusal('mirror_source_or_config_changed') |
| 317 | if inspect_target(target, pair) != observed: |
| 318 | raise Refusal('mirror_target_changed_before_commit') |
| 319 | git(target, 'update-ref', 'refs/heads/' + plan['destination']['branch'], commit, |
| 320 | '0' * 40 if observed == 'absent' else observed) |
| 321 | result['export'] = 'mapped_no_content_change' if same_tree else 'prepared' |
| 322 | observed = commit |
| 323 | else: |
| 324 | result['export'] = 'no_change_verified' |
| 325 | result['git_head'] = observed |
| 326 | # Recover an interrupted record write using exact tree, parent and trailers. |
| 327 | if not correspondence(target, observed, plan, plan_hash, files): |
| 328 | raise Refusal('mirror_correspondence_failed') |
| 329 | if operation != 'verify': |
| 330 | save_record(root, old, pair, plan, plan_hash, observed) |
| 331 | if operation == 'deliver': |
| 332 | if transport is None: |
| 333 | raise Refusal('mirror_delivery_transport_missing') |
| 334 | deliver(result, transport, lambda: recheck(root, config, original, plan, pair, projection, observed)) |
| 335 | else: |
| 336 | result['ok'] = True |
| 337 | except (Refusal, OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as exc: |
| 338 | result['error'] = str(exc) if isinstance(exc, Refusal) else type(exc).__name__ |
| 339 | return result |
| 340 | |
| 341 | |
| 342 | def recheck(root, config, original, plan, pair, projection, commit): |
| 343 | if read(root, '.overseer/config.yaml') != original or check_source(root, config, plan) != projection: |
| 344 | raise Refusal('mirror_source_or_config_changed') |
| 345 | if inspect_target(Path(plan['target']), pair) != commit: |
| 346 | raise Refusal('mirror_target_drift_before_delivery') |
| 347 | verify_tree(Path(plan['target']), commit, projection['files']) |
| 348 | |
| 349 | |
| 350 | def deliver(result, transport, revalidate): |
| 351 | """Delivery state is observed every time; an earlier success flag is never proof.""" |
| 352 | source, dest, commit = result['source'], result['destination'], result['git_head'] |
| 353 | def observe_authority(): |
| 354 | result['authority'] = 'outcome_unknown' |
| 355 | value = transport.authority(source) |
| 356 | if value != (source['repo_id'], source['revision']): |
| 357 | result['authority'] = 'unknown_or_drifted' |
| 358 | raise Refusal('mirror_authority_unknown_or_drifted') |
| 359 | result['authority'] = 'observed_matching' |
| 360 | return value |
| 361 | def observe_remote(): |
| 362 | value = transport.remote_head(dest) |
| 363 | result['remote_head'] = value |
| 364 | return value |
| 365 | observation = observe_authority() |
| 366 | revalidate() |
| 367 | remote = observe_remote() |
| 368 | if remote not in {dest['expected_head'], commit}: |
| 369 | raise Refusal('mirror_remote_head_drift') |
| 370 | if remote != commit: |
| 371 | # Repeat immediately before the write, including the authoritative source. |
| 372 | revalidate() |
| 373 | if observe_authority() != observation or observe_remote() != remote: |
| 374 | raise Refusal('mirror_remote_changed_before_push') |
| 375 | result['push'] = 'outcome_unknown' |
| 376 | try: |
| 377 | transport.push(Path(result['target']), dest, commit) |
| 378 | except (Refusal, OSError, subprocess.SubprocessError): |
| 379 | # A lost response may follow a successful push. Readback resolves it. |
| 380 | result['push'] = 'interrupted' |
| 381 | if observe_remote() != commit: |
| 382 | result['push'] = 'pending_retry' |
| 383 | raise Refusal('mirror_push_incomplete') |
| 384 | result['push'] = 'verified' |
| 385 | else: |
| 386 | result['push'] = 'already_delivered' |
| 387 | revalidate() |
| 388 | if observe_authority() != observation or observe_remote() != commit: |
| 389 | result['push'] = 'remote_drift_after_push' |
| 390 | raise Refusal('mirror_remote_changed_before_pr') |
| 391 | result['pr'] = 'pending_retry' |
| 392 | result['pr_url'] = transport.ensure_pr(Path(result['target']), dest, source, commit) |
| 393 | result['pr'] = 'verified' |
| 394 | result['ok'] = True |
| 395 | |
| 396 | |
| 397 | class NetworkDelivery: |
| 398 | """Explicit delivery adapter; tests substitute transport, never real remotes. |
| 399 | |
| 400 | Only public staging JSON refs are supported. Private/authenticated MuseHub and |
| 401 | production trust are outside R2. No automatic login, trust reset or URL fallback. |
| 402 | """ |
| 403 | def __init__(self, gh): |
| 404 | self.gh = physical(gh) |
| 405 | if not self.gh.is_file() or not os.access(self.gh, os.X_OK): |
| 406 | raise Refusal('mirror_gh_executable_required') |
| 407 | |
| 408 | def authority(self, source): |
| 409 | class NoRedirect(urllib.request.HTTPRedirectHandler): |
| 410 | def redirect_request(self, *args, **kwargs): |
| 411 | raise Refusal('mirror_authority_redirect_refused') |
| 412 | request = urllib.request.Request(source['hub_url'] + '/refs', headers={'Accept': 'application/json'}) |
| 413 | with urllib.request.build_opener(NoRedirect).open(request, timeout=15) as response: |
| 414 | raw = response.read(2 * 1024 * 1024 + 1) |
| 415 | if len(raw) > 2 * 1024 * 1024: |
| 416 | raise Refusal('mirror_authority_response_too_large') |
| 417 | value = json.loads(raw, object_pairs_hook=unique_json) |
| 418 | if not isinstance(value, dict) or value.get('domain') != 'code': |
| 419 | raise Refusal('mirror_authority_domain_mismatch') |
| 420 | heads = value.get('branch_heads') |
| 421 | if not isinstance(heads, dict): |
| 422 | raise Refusal('mirror_authority_refs_invalid') |
| 423 | return value.get('repo_id'), heads.get(source['branch']) |
| 424 | |
| 425 | def remote_head(self, dest): |
| 426 | raw = git(Path('/'), 'ls-remote', '--refs', dest['url'], 'refs/heads/' + dest['branch']) |
| 427 | rows = raw.decode().splitlines() |
| 428 | if not rows: |
| 429 | return 'absent' |
| 430 | fields = rows[0].split('\t') |
| 431 | if len(rows) != 1 or len(fields) != 2 or fields[1] != 'refs/heads/' + dest['branch'] or not HEX.fullmatch(fields[0]): |
| 432 | raise Refusal('mirror_remote_response_invalid') |
| 433 | return rows[0].split('\t')[0] |
| 434 | |
| 435 | def push(self, target, dest, commit): |
| 436 | git(target, 'push', '--porcelain', dest['url'], commit + ':refs/heads/' + dest['branch']) |
| 437 | |
| 438 | def gh_run(self, target, *args): |
| 439 | proc = subprocess.run([str(self.gh), *args], cwd=target, env=env(), capture_output=True, timeout=30) |
| 440 | if proc.returncode: |
| 441 | raise Refusal('mirror_pr_command_failed') |
| 442 | return proc.stdout |
| 443 | |
| 444 | def ensure_pr(self, target, dest, source, commit): |
| 445 | args = ('pr', 'list', '--repo', dest['repository'], '--head', dest['branch'], '--base', dest['base'], |
| 446 | '--state', 'open', '--json', 'url,headRefOid,isCrossRepository') |
| 447 | def observe(): |
| 448 | rows = json.loads(self.gh_run(target, *args)) |
| 449 | if len(rows) > 1: |
| 450 | raise Refusal('mirror_pr_ambiguous') |
| 451 | if rows and (rows[0]['headRefOid'] != commit or rows[0]['isCrossRepository']): |
| 452 | raise Refusal('mirror_pr_head_mismatch') |
| 453 | return rows[0]['url'] if rows else None |
| 454 | url = observe() |
| 455 | if not url: |
| 456 | # A file preserves literal newlines and avoids shell interpretation. |
| 457 | with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') as body: |
| 458 | body.write(f'Approved Muse snapshot: {source["revision"]}\nSource: {source["hub_url"]}\nGit mirror: {commit}\n') |
| 459 | body.flush() |
| 460 | self.gh_run(target, 'pr', 'create', '--repo', dest['repository'], '--head', dest['branch'], |
| 461 | '--base', dest['base'], '--title', 'Mirror approved Muse snapshot', '--body-file', body.name) |
| 462 | url = observe() |
| 463 | if not isinstance(url, str) or not re.fullmatch( |
| 464 | re.escape('https://github.com/' + dest['repository'] + '/pull/') + r'[1-9][0-9]*', url): |
| 465 | raise Refusal('mirror_pr_not_verified') |
| 466 | return url |
| 467 | |
| 468 | |
| 469 | def command(root, config, original, args): |
| 470 | raw = read(root, args.plan_file) |
| 471 | if digest(raw) != args.expect_plan: |
| 472 | raise Refusal('mirror_plan_digest_mismatch') |
| 473 | plan = json.loads(raw, object_pairs_hook=unique_json) |
| 474 | try: |
| 475 | validate_plan(plan, root, config) |
| 476 | except (TypeError, KeyError, AttributeError): |
| 477 | raise Refusal('mirror_plan_invalid') from None |
| 478 | transport = NetworkDelivery(args.gh) if args.operation == 'deliver' else None |
| 479 | return execute(root, config, original, plan, digest(raw), args.operation, transport) |
File History
1 commit
sha256:c87cf2c2f03da507f66f93922cb339a2cbebb33ad6372a6039032fb09a20da12
Reconcile reviewed Overseer recovery d47291d5d9030de5ebef71…
Human
minor
⚠
11 days ago