#!/usr/bin/env python3 """Build a versioned flat operations bundle from committed files, without touching the worktree/index.""" import argparse import hashlib import json import os from pathlib import Path import re import subprocess import tempfile FILES=('stage.php','promote.php','operations.php','rollback.php','readiness.php', 'verify-node.py','panel-acceptance.php','install-node.sh','renew-node.sh','dpl-node.service') def build(repo,source,base,branch,bundle,advance=False): if not all(re.fullmatch('[0-9a-f]{40}',r) for r in (source,base)) or not re.fullmatch('codex/[A-Za-z0-9_/-]+',branch) or '..' in branch: raise ValueError('Explicit full commits and a new codex branch required') destination=Path(bundle) if not destination.is_absolute() or destination.exists() or destination.is_symlink():raise ValueError('New absolute bundle path required') def git(*args,input=None,env=None): r=subprocess.run(['git','-C',str(repo),*args],input=input,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,env=env) if r.returncode:raise ValueError('Git source/base/ref check failed') return r.stdout git('cat-file','-e',source+'^{commit}');git('cat-file','-e',base+'^{commit}');git('check-ref-format','refs/heads/'+branch) exists=subprocess.run(['git','-C',str(repo),'show-ref','--verify','--quiet','refs/heads/'+branch]).returncode expected_old='0'*40 if exists==0 and advance: expected_old=git('rev-parse','refs/heads/'+branch).decode().strip() if expected_old!=base:raise ValueError('Advance requires the exact current branch head as reviewed base') elif exists!=1:raise ValueError('Branch exists or outcome unknown; inspect it, do not overwrite') with tempfile.TemporaryDirectory(prefix='dpl-ops-index-',dir=destination.parent) as temp: env=dict(os.environ,GIT_INDEX_FILE=str(Path(temp)/'index')) git('read-tree',base,env=env) for name in FILES: entry=git('ls-tree',source,'--','scripts/node-onboarding/'+name).decode().strip() if not entry:raise ValueError('Required committed operations file missing') mode,kind,rest=entry.split(' ',2);sha,_=rest.split('\t',1) if kind!='blob' or mode not in ('100644','100755'):raise ValueError('Regular committed operations file required') git('update-index','--add','--cacheinfo',mode+','+sha+','+name,env=env) tree=git('write-tree',env=env).decode().strip() commit=git('commit-tree',tree,'-p',base,input=('DPL operations snapshot\n\nSource commit: '+source+'\n').encode()).decode().strip() git('update-ref','refs/heads/'+branch,commit,expected_old) git('bundle','create',str(destination),'refs/heads/'+branch) destination.chmod(0o600) return {'commit':commit,'source_commit':source,'branch':branch,'bundle_sha256':hashlib.sha256(destination.read_bytes()).hexdigest(),'files':len(FILES),'network':False} def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('--repo',default=str(Path(__file__).resolve().parents[2])) for k in ('source','base','branch','bundle'):p.add_argument('--'+k,required=True) p.add_argument('--advance',action='store_true',help='Explicit fast-forward only from the exact --base current branch head') a=p.parse_args() try: print(json.dumps(build(a.repo,a.source,a.base,a.branch,a.bundle,a.advance)));return 0 except Exception: print(json.dumps({'state':'blocked','reason':'Inspect source/base, branch and bundle outcome before retry; no network performed'}));return 2 if __name__=='__main__':raise SystemExit(main())