dpl/scripts/node-onboarding/bundle-ops.py
hellcat 5c9157f5d7
Some checks are pending
Docker Build and Publish / build (push) Waiting to run
feat: add reusable jump-host node onboarding runtime
2026-10-17 15:22:30 +08:00

56 lines
3.2 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):
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
if 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,'0'*40)
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)
a=p.parse_args()
try:
print(json.dumps(build(a.repo,a.source,a.base,a.branch,a.bundle)));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())