dpl/scripts/node-onboarding/bundle-ops.py
hellcat e26b5bc6ca
Some checks are pending
Docker Build and Publish / build (push) Waiting to run
fix: finalize acceptance identity and raw QA review evidence
2026-10-17 15:32:03 +08:00

61 lines
3.6 KiB
Python

#!/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())