| workflow_payload |
name: Runner maintenance storage preflight (read o name: Runner maintenance storage preflight (read only)
"on":
push:
branches: [codex/runner-maintenance-diagnostic-20261101]
paths: [.gitea/workflows/runner-maintenance-preflight.yml]
workflow_dispatch:
jobs:
preflight:
name: Verify actual storage capacity and schema metadata
runs-on: ubuntu-latest
if: github.repository == 'luoanwu/enterprise-platform'
steps:
- name: Read storage sizes and public build metadata
run: |
set -eu
python3 <<'PY'
import configparser, datetime, hashlib, json, mmap, os, pathlib, socket, sqlite3, struct, subprocess
if os.geteuid()!=0 or socket.gethostname()!='iZ2zed3pt2nygf9dy1a9d2Z': raise RuntimeError('EXPECTED_HOST_REQUIRED')
config_path=pathlib.Path('/home/git/gitea/config/app.ini')
if hashlib.sha256(config_path.read_bytes()).hexdigest()!='8923b93032ec5c303317b02a7011f756c0ee8e6c2756d4cd4286dd9b5c54d072': raise RuntimeError('CONFIG_CHANGED_REINVENTORY_REQUIRED')
report={'kind':'RunnerMaintenanceStoragePreflight','observedAtUTC':datetime.datetime.now(datetime.timezone.utc).isoformat(),'ci':{'sourceSha':os.environ.get('GITHUB_SHA'),'runId':os.environ.get('GITHUB_RUN_ID')},'releaseEligible':False,'sizes':[],'storageSections':[]}
def command(args,timeout=40):
r=subprocess.run(args,stdin=subprocess.DEVNULL,capture_output=True,text=True,timeout=timeout)
return r.returncode,r.stdout
for p in ['/home/git/gitea','/home/git/repositories','/usr/local/bin/data','/opt/gitea-runner','/root/gitea-ext','/root/gitea-code-review']:
if not pathlib.Path(p).exists(): continue
code,out=command(['du','-sx','--block-size=1',p])
report['sizes'].append({'path':p,'bytes':int(out.split()[0]) if code==0 else None,'exitCode':code})
st=os.statvfs('/')
report['rootFilesystem']={'availableBytes':st.f_bavail*st.f_frsize,'totalBytes':st.f_blocks*st.f_frsize}
# Gitea INI permits top-level APP_NAME/RUN_USER before its first section.
cfg=configparser.RawConfigParser(strict=False);cfg.read_string('[DEFAULT]\n'+config_path.read_text())
for section in cfg.sections():
if section.lower().startswith(('storage','lfs','attachment','packages','actions')):
report['storageSections'].append({'section':section,'keys':[k for k in cfg[section] if not any(x in k.lower() for x in ['secret','password','token','access_key'])]})
db='/home/git/gitea/data/gitea.db'
connection=sqlite3.connect('file:'+db+'?mode=ro',uri=True,timeout=10)
connection.execute('PRAGMA query_only=ON')
tables={r[0] for r in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
report['database']={'path':db,'sqliteVersion':sqlite3.sqlite_version,'tableCount':len(tables),'hasAttemptTable':'action_run_attempt' in tables}
if 'version' in tables:
columns=[r[1] for r in connection.execute('PRAGMA table_info("version")')]
col=next((x for x in columns if x.lower()=='version'),None)
if col: report['database']['schemaVersions']=[r[0] for r in connection.execute('SELECT "'+col.replace('"','""')+'" FROM "version" LIMIT 5')]
report['database']['journalMode']=connection.execute('PRAGMA journal_mode').fetchone()[0]
connection.close()
def go_build(path):
try:
with open(path,'rb') as f, mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) as data:
at=data.find(b'\xff Go buildinf:')
if at<0:return {'format':'not-found'}
ptr_size=data[at+14];flags=data[at+15]
if not flags&2:return {'format':'pointer-layout','pointerSize':ptr_size,'flags':flags}
pos=at+32
def readvar(pos):
value=0;shift=0
for i in range(10):
b=data[pos];pos+=1;value|=(b&127)<<shift
if b<128:return value,pos
shift+=7
raise ValueError('invalid-varint')
size,pos=readvar(pos);version=bytes(data[pos:pos+size]).decode(errors='replace');pos+=size
size,pos=readvar(pos);mod=bytes(data[pos:pos+size]).decode(errors='replace')
selected=[]
for line in mod.splitlines():
if line.startswith('path\t') and ('gitea' in line or 'runner' in line): selected.append(line[:300])
elif line.startswith('mod\t') and ('gitea' in line or 'runner' in line): selected.append(line[:300])
elif line.startswith('build\tvcs'): selected.append(line[:200])
return {'format':'inline','goVersion':version,'selectedModuleFields':selected}
except Exception as e:return {'errorClass':type(e).__name__}
report['giteaGoBuild']=go_build('/usr/local/bin/gitea')
report['runnerGoBuild']=go_build('/opt/gitea-runner/act-runner')
for p in ['/home/git/gitea/custom','/home/git/gitea/config','/usr/local/bin/data','/opt/gitea-runner']:
root=pathlib.Path(p)
if not root.is_dir():continue
children=[]
for child in sorted(root.iterdir()):
if child.name.startswith('.') or any(x in child.name.lower() for x in ['secret','token','password','credential','private']):continue
if p=='/opt/gitea-runner' and child.name not in ['config.yaml','config.yml','act-runner','act_runner']:continue
children.append({'name':child.name,'directory':child.is_dir(),'bytes':child.stat().st_size})
report.setdefault('layoutMetadata',[]).append({'path':p,'children':children[:30]})
report['sourceCandidates']=[]
for base in ['/root','/opt','/home/git','/usr/local/src','/srv']:
root=pathlib.Path(base)
if not root.is_dir():continue
for candidate in root.iterdir():
if not candidate.is_dir() or 'gitea' not in candidate.name.lower() or candidate.name.startswith('.'):continue
for project in [candidate,candidate/'gitea',candidate/'src']:
if not (project/'.git').exists():continue
code,head=command(['git','--no-optional-locks','-C',str(project),'rev-parse','HEAD'])
sc,dirty=command(['git','--no-optional-locks','-C',str(project),'status','--porcelain','--untracked-files=no'])
rc,origin=command(['git','--no-optional-locks','-C',str(project),'remote','get-url','origin'])
import re
safe_origin=re.sub(r'://[^/@]+@','://REDACTED@',origin.strip()).split('?',1)[0]
report['sourceCandidates'].append({'path':str(project),'head':head.strip() if code==0 else None,'trackedDirty':bool(dirty.strip()) if sc==0 else None,'origin':safe_origin if rc==0 else None})
report['sizeBreakdown']=[]
for parent in ['/home/git/gitea','/root/gitea-code-review']:
for child in pathlib.Path(parent).iterdir():
if child.name.startswith('.') or any(x in child.name.lower() for x in ['secret','credential','token','password','private']):continue
if not child.is_dir():continue
code,out=command(['du','-sx','--block-size=1',str(child)])
report['sizeBreakdown'].append({'path':str(child),'bytes':int(out.split()[0]) if code==0 else None})
cfgfile=pathlib.Path('/opt/gitea-runner/config.yaml')
if cfgfile.is_file():
selected={};parent=None
allowed={'log':{'level'},'runner':{'capacity','timeout','shutdown_timeout','fetch_timeout','fetch_interval','file'},'container':{'network','privileged','docker_host'},'cache':{'enabled','dir','host','port'}}
for line in cfgfile.read_text().splitlines():
if not line.strip() or line.lstrip().startswith('#'):continue
if not line.startswith((' ','\t')):parent=line.split(':',1)[0].strip();continue
if ':' not in line:continue
key,value=line.strip().split(':',1)
if key in allowed.get(parent,set()):selected[parent+'.'+key]=value.strip()[:200]
report['runnerConfigWhitelist']=selected
report['boundaries']={'serviceChanges':False,'dataWrites':False,'secretValuesPrinted':False,'backupsOrUpgrades':False}
print('RUNNER_MAINTENANCE_PREFLIGHT_JSON '+json.dumps(report,separators=(',',':')))
PY
shell: bash
timeout-minutes: "5"
permissions:
contents: read
... |