| workflow_payload |
name: Gitea private upgrade preparation
"on name: Gitea private upgrade preparation
"on":
push:
branches: [codex/runner-maintenance-diagnostic-20261101]
paths: [.gitea/workflows/gitea-upgrade-stage.yml]
workflow_dispatch:
jobs:
prepare:
name: prepare
runs-on: ubuntu-latest
if: github.repository == 'luoanwu/enterprise-platform'
steps:
- name: Preserve reproduced baseline and prepare isolated compatibility analysis
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/('stage-'+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':'GiteaUpgradePrivatePreparation','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_PREPARATION_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=work/'baseline-source';snapshot.mkdir(mode=0o700)
for p in paths:
target=snapshot/p.relative_to(source);target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(p,target)
shutil.copy2('/usr/local/bin/gitea',work/'gitea.previous')
bundle=git(['bundle','create',str(work/'baseline.bundle'),'HEAD'])
if bundle.returncode:raise RuntimeError('SOURCE_BUNDLE_FAILED')
patch=git(['diff','--binary','HEAD'])
if patch.returncode:raise RuntimeError('SOURCE_PATCH_FAILED')
(work/'baseline-working.patch').write_bytes(patch.stdout)
if fingerprint(paths)!=before:raise RuntimeError('SOURCE_CHANGED_DURING_SNAPSHOT')
report['baselineSnapshotFiles']=len(paths);report['baselineBinarySha256']=sha(work/'gitea.previous');report['baselineBundleSha256']=sha(work/'baseline.bundle');report['baselinePatchSha256']=sha(work/'baseline-working.patch')
emit('BASELINE_SAVED')
dbpath=work/'online-database-snapshot.sqlite'
original=sqlite3.connect('file:/home/git/gitea/data/gitea.db?mode=ro',uri=True,timeout=15)
original.execute('PRAGMA query_only=ON')
clone=sqlite3.connect(dbpath)
deadline=time.monotonic()+300
def progress(status,remaining,total):
if time.monotonic()>deadline:raise RuntimeError('DATABASE_SNAPSHOT_DEADLINE')
try:original.backup(clone,pages=1024,progress=progress,sleep=0.02)
finally:clone.close();original.close()
os.chmod(dbpath,0o600)
check=sqlite3.connect('file:'+str(dbpath)+'?mode=ro',uri=True)
integrity=[r[0] for r in check.execute('PRAGMA integrity_check')]
if integrity!=['ok']:raise RuntimeError('SNAPSHOT_INTEGRITY_FAILED')
report['databaseSnapshot']={'sha256':sha(dbpath),'bytes':dbpath.stat().st_size,'integrityCheck':'ok','schemaVersion':check.execute('SELECT version FROM version WHERE id=1').fetchone()[0],'scope':'SQLite online snapshot only; files not at same stop-write point'}
check.close();emit('ONLINE_DATABASE_SNAPSHOT_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})
report['status']='PRIVATE_BASELINE_AND_SNAPSHOT_PREPARED'
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: "20"
permissions:
contents: read
... |