| workflow_payload |
name: Gitea private compatibility analysis
"on name: Gitea private compatibility analysis
"on":
push:
branches: [codex/runner-maintenance-diagnostic-20261101]
paths: [.gitea/workflows/gitea-upgrade-analysis.yml]
workflow_dispatch:
jobs:
analyze:
name: analyze
runs-on: ubuntu-latest
if: github.repository == 'luoanwu/enterprise-platform'
steps:
- name: Verify saved baseline and analyze official fixed-source compatibility
run: |
set -eu
python3 <<'PY'
import datetime, hashlib, json, os, pathlib, re, shutil, socket, sqlite3, subprocess, time
if os.geteuid()!=0 or socket.gethostname()!='iZ2zed3pt2nygf9dy1a9d2Z':raise RuntimeError('EXPECTED_HOST_REQUIRED')
os.umask(0o077)
runid=os.environ.get('GITHUB_RUN_ID')
if not runid or not runid.isdigit():raise RuntimeError('REAL_RUN_ID_REQUIRED')
source=pathlib.Path('/root/gitea-src')
def sha(p):
h=hashlib.sha256()
with open(p,'rb') as f:
for b in iter(lambda:f.read(1024*1024),b''):h.update(b)
return h.hexdigest()
if sha('/usr/local/bin/gitea')!='640efd1256fe5ace902a1185bd6fdedd063ec1eec08422aceded8cc9559a8dc9':raise RuntimeError('INSTALLED_BINARY_CHANGED')
def source_files():
paths=[]
for root,dirs,files in os.walk(source):
dirs[:]=sorted(d for d in dirs if d not in ['.git','node_modules','.cache','.secrets','.evidence-private'])
for name in sorted(files):
if name.startswith('.env') or name.endswith(('.key','.pem')):continue
p=pathlib.Path(root)/name
if not p.is_symlink() and p.is_file():paths.append(p)
return paths
def fingerprint(paths):
h=hashlib.sha256()
for p in paths:h.update(str(p.relative_to(source)).encode()+b'\0'+bytes.fromhex(sha(p)))
return h.hexdigest()
paths=source_files();before=fingerprint(paths)
if before!='a5534f2aa03220a02efecac6753f0edcb99db7e8a3da323a1bac3390e2793d74':raise RuntimeError('REPRODUCED_SOURCE_CHANGED')
base=pathlib.Path('/var/lib/eios-r1-maintenance')
if base.is_symlink() or not base.is_dir() or base.stat().st_uid!=0 or base.stat().st_mode&0o077:raise RuntimeError('PRIVATE_BASE_REQUIRED')
if shutil.disk_usage(base).free<40*1024**3:raise RuntimeError('STAGING_FREE_SPACE_REQUIRED')
work=base/('analysis-'+runid);work.mkdir(mode=0o700)
env={'PATH':'/usr/local/bin:/usr/bin:/bin','HOME':str(work),'LANG':'C.UTF-8','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0','GIT_OPTIONAL_LOCKS':'0'}
report={'kind':'GiteaUpgradeCompatibilityAnalysis','observedAtUTC':datetime.datetime.now(datetime.timezone.utc).isoformat(),'ci':{'sourceSha':os.environ.get('GITHUB_SHA'),'runId':runid},'releaseEligible':False,'privateWorkspace':str(work),'liveServiceChanges':False,'liveDatabaseWrites':False,'fullConsistentBackup':False,'sourceFingerprint':before}
def emit(stage):
report['stage']=stage;print('GITEA_UPGRADE_ANALYSIS_JSON '+json.dumps(report,separators=(',',':')),flush=True)
def git(args,cwd=source,timeout=300):
return subprocess.run(['git','-c','credential.helper=','-c','core.askPass=','-c','http.extraHeader=','-c','core.hooksPath=/dev/null',*args],cwd=cwd,env=env,stdin=subprocess.DEVNULL,capture_output=True,timeout=timeout)
emit('PREPARING')
snapshot=base/'stage-26762'/'baseline-source'
h=hashlib.sha256()
for p in paths:
target=snapshot/p.relative_to(source)
if target.is_symlink() or not target.is_file():raise RuntimeError('BASELINE_SNAPSHOT_MISSING')
h.update(str(p.relative_to(source)).encode()+b'\0'+bytes.fromhex(sha(target)))
if h.hexdigest()!=before:raise RuntimeError('BASELINE_SNAPSHOT_FINGERPRINT_MISMATCH')
parent=base/'stage-26762'
if sha(parent/'gitea.previous')!='640efd1256fe5ace902a1185bd6fdedd063ec1eec08422aceded8cc9559a8dc9':raise RuntimeError('BASELINE_BINARY_MISMATCH')
verify=git(['bundle','verify',str(parent/'baseline.bundle')])
if verify.returncode:raise RuntimeError('BASELINE_BUNDLE_VERIFY_FAILED')
report['verifiedBaseline']={'path':str(parent),'sourceFingerprint':h.hexdigest(),'bundleSha256':sha(parent/'baseline.bundle'),'patchSha256':sha(parent/'baseline-working.patch'),'binarySha256':sha(parent/'gitea.previous')}
report['databaseSnapshot']='NOT_OBTAINED; prior online backup reached deadline; no retry in this job'
emit('BASELINE_FILES_AND_BUNDLE_VERIFIED')
upstream=work/'candidate-source'
r=git(['clone','--depth','1','--branch','v1.27.3','--filter=blob:none','https://github.com/go-gitea/gitea.git',str(upstream)],cwd=work,timeout=600)
(work/'upstream-fetch.stdout').write_bytes(r.stdout);(work/'upstream-fetch.stderr').write_bytes(r.stderr)
if r.returncode:
report['status']='UPSTREAM_FETCH_FAILED';report['fetchExitCode']=r.returncode;emit('STOPPED_BEFORE_CANDIDATE_BUILD');raise SystemExit(0)
head=git(['rev-parse','HEAD'],cwd=upstream)
if head.returncode or head.stdout.decode().strip()!='146cc3eec57174711eac0e0a0c7b38670c6e3922':raise RuntimeError('FIXED_UPSTREAM_COMMIT_MISMATCH')
report['upstream']={'tag':'v1.27.3','commit':head.stdout.decode().strip(),'purpose':'compatibility analysis candidate, not selected for deployment'}
changed=git(['diff','--name-only','--diff-filter=ACMRT','HEAD'])
if changed.returncode:raise RuntimeError('SOURCE_CHANGED_PATHS_UNAVAILABLE')
report['customizationMerge']=[]
merge=work/'merge-inputs';merge.mkdir(mode=0o700)
for i,name in enumerate(changed.stdout.decode().splitlines()):
rel=pathlib.PurePosixPath(name)
if rel.is_absolute() or '..' in rel.parts:raise RuntimeError('UNSAFE_SOURCE_PATH')
# Existing migration edits are retained in the baseline; do not transplant the legacy migration list.
if name.startswith('models/migrations/'):
report['customizationMerge'].append({'path':name,'status':'HELD_FOR_EXPLICIT_MIGRATION_BRIDGE'});continue
target=upstream/name
if not target.is_file() or target.is_symlink():
report['customizationMerge'].append({'path':name,'status':'TARGET_PATH_CHANGED_REVIEW_REQUIRED'});continue
original_blob=git(['show','HEAD:'+name])
if original_blob.returncode:raise RuntimeError('BASE_BLOB_READ_FAILED')
theirs=(source/name).read_bytes().replace(b'code.gitea.io/gitea',b'gitea.dev')
ancestor=original_blob.stdout.replace(b'code.gitea.io/gitea',b'gitea.dev')
a=merge/(str(i)+'.ours');b=merge/(str(i)+'.base');c=merge/(str(i)+'.theirs')
a.write_bytes(target.read_bytes());b.write_bytes(ancestor);c.write_bytes(theirs)
m=git(['merge-file','-p',str(a),str(b),str(c)],cwd=work,timeout=30)
if m.returncode==0:
target.write_bytes(m.stdout);state='TEXTUAL_MERGE_CLEAN_SEMANTICS_UNVERIFIED'
elif 1<=m.returncode<=127:
(merge/(str(i)+'.conflicted')).write_bytes(m.stdout);state='SEMANTIC_REVIEW_REQUIRED'
else:state='MERGE_TOOL_FAILED'
report['customizationMerge'].append({'path':name,'status':state})
untracked=git(['ls-files','--others','--exclude-standard'])
if untracked.returncode:raise RuntimeError('UNTRACKED_SOURCE_INVENTORY_FAILED')
report['untrackedCustomization']=[]
for name in untracked.stdout.decode().splitlines():
rel=pathlib.PurePosixPath(name)
if rel.is_absolute() or '..' in rel.parts:raise RuntimeError('UNSAFE_UNTRACKED_PATH')
if not rel.parts or rel.parts[0] not in ['models','routers','services','modules','templates','options']:continue
if rel.suffix not in ['.go','.tmpl','.json']:continue
p=source/name;target=upstream/name
if p.is_symlink() or not p.is_file():continue
if target.exists():state='COLLISION_REVIEW_REQUIRED'
else:
target.parent.mkdir(parents=True,exist_ok=True)
target.write_bytes(p.read_bytes().replace(b'code.gitea.io/gitea',b'gitea.dev'))
state='PRESERVED_FROM_REPRODUCED_BASELINE_SEMANTICS_UNVERIFIED'
report['untrackedCustomization'].append({'path':name,'status':state})
report['status']='PRIVATE_BASELINE_VERIFIED_AND_COMPATIBILITY_ANALYZED'
report['candidateBuilt']=False;report['candidateMigrationExecuted']=False;report['sourceFingerprintAfter']=fingerprint(paths)
if report['sourceFingerprintAfter']!=before:raise RuntimeError('LIVE_SOURCE_CHANGED_DURING_PREPARATION')
(work/'receipt.json').write_text(json.dumps(report,indent=2)+'\n')
emit('PREPARED_FOR_REVIEW')
PY
shell: bash
timeout-minutes: "15"
permissions:
contents: read
... |