| workflow_payload |
name: Gitea offline build identity check
"on name: Gitea offline build identity check
"on":
push:
branches: [codex/runner-maintenance-diagnostic-20261101]
paths: [.gitea/workflows/gitea-offline-repro.yml]
workflow_dispatch:
jobs:
reproduce:
name: Verify current custom build without service changes
runs-on: ubuntu-latest
if: github.repository == 'luoanwu/enterprise-platform'
steps:
- name: Reproduce offline in private resource-limited workspace
run: |
set -eu
python3 <<'PY'
import datetime, hashlib, json, mmap, os, pathlib, socket, subprocess, re
if os.geteuid()!=0 or socket.gethostname()!='iZ2zed3pt2nygf9dy1a9d2Z':raise RuntimeError('EXPECTED_HOST_REQUIRED')
source=pathlib.Path('/root/gitea-src')
binary=pathlib.Path('/usr/local/bin/gitea')
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(binary)!='640efd1256fe5ace902a1185bd6fdedd063ec1eec08422aceded8cc9559a8dc9':raise RuntimeError('RUNNING_BINARY_CHANGED')
with open(binary,'rb') as f,mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ) as data:
at=data.find(b'\xff Go buildinf:')
if at<0 or not data[at+15]&2:raise RuntimeError('INLINE_BUILD_INFO_REQUIRED')
def varint(pos):
n=0;shift=0
for i in range(10):
b=data[pos];pos+=1;n|=(b&127)<<shift
if b<128:return n,pos
shift+=7
raise RuntimeError('INVALID_BUILD_INFO')
pos=at+32;n,pos=varint(pos);goversion=bytes(data[pos:pos+n]).decode();pos+=n
n,pos=varint(pos);module=bytes(data[pos:pos+n]).decode(errors='replace')
settings={}
for line in module.splitlines():
if line.startswith('build\t') and '=' in line[6:]:
k,v=line[6:].split('=',1);settings[k]=v
report={'kind':'GiteaOfflineReproducibilityProbe','observedAtUTC':datetime.datetime.now(datetime.timezone.utc).isoformat(),'ci':{'sourceSha':os.environ.get('GITHUB_SHA'),'runId':os.environ.get('GITHUB_RUN_ID')},'releaseEligible':False,'requiredGoVersion':goversion,'status':'PREPARING','liveServicesChanged':False,'liveDataChanged':False,'sourceWritten':False,'newCredentials':False}
candidates=[pathlib.Path('/usr/local/go/bin/go')]
for root in ['/root/go/pkg/mod/golang.org','/root/sdk','/usr/local','/opt']:
p=pathlib.Path(root)
if not p.is_dir():continue
for d in p.iterdir():
if d.is_dir() and (d.name.startswith('toolchain@') or d.name.startswith('go')):
candidates.append(d/'bin/go')
compiler=None
report['toolchains']=[]
for p in dict.fromkeys(candidates):
if not p.is_file():continue
r=subprocess.run([str(p),'version'],capture_output=True,text=True,timeout=10,env={'PATH':'/usr/bin:/bin','GOTOOLCHAIN':'local'})
text=r.stdout.strip() if r.returncode==0 else ''
report['toolchains'].append({'path':str(p),'version':text[:150]})
if goversion in text.split():compiler=p
def emit():
print('GITEA_OFFLINE_REPRO_JSON '+json.dumps(report,separators=(',',':')))
if compiler is None:
report['status']='BLOCKED_MATCHING_TOOLCHAIN_NOT_FOUND';emit();raise SystemExit(0)
if settings.get('-buildmode','exe')!='exe' or settings.get('-compiler','gc')!='gc':
report['status']='BLOCKED_UNSUPPORTED_BUILD_SETTINGS';emit();raise SystemExit(0)
linkflags=settings.get('-ldflags','')
if re.search(r'password|secret|token|private.?key',linkflags,re.I):
report['status']='BLOCKED_SENSITIVE_LINK_FLAGS';emit();raise SystemExit(0)
def fingerprint():
h=hashlib.sha256();count=0
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 p.is_symlink() or not p.is_file():continue
h.update(str(p.relative_to(source)).encode()+b'\0'+bytes.fromhex(sha(p)));count+=1
return h.hexdigest(),count
before,count=fingerprint();report['inputFingerprintBefore']=before;report['inputFiles']=count
base=pathlib.Path('/var/lib/eios-r1-maintenance')
if base.exists() and (base.is_symlink() or base.stat().st_uid!=0 or base.stat().st_mode&0o077):raise RuntimeError('PRIVATE_WORKSPACE_PERMISSIONS_REQUIRED')
base.mkdir(mode=0o700,exist_ok=True)
runid=os.environ.get('GITHUB_RUN_ID')
if not runid or not runid.isdigit():raise RuntimeError('REAL_RUN_ID_REQUIRED')
work=base/('repro-'+runid);work.mkdir(mode=0o700)
for name in ['cache','tmp']:(work/name).mkdir(mode=0o700)
args=[str(compiler),'build','-mod=readonly','-p','2','-o',str(work/'gitea')]
for setting in ['-tags','-trimpath','-asmflags','-gcflags','-ldflags']:
if setting in settings:args.append(setting+'='+settings[setting])
args.append('.')
unit='eios-r1-repro-'+runid
command=['systemd-run','--quiet','--wait','--collect','--unit='+unit,'--working-directory='+str(source),
'--property=User=root','--property=Nice=10','--property=CPUQuota=150%','--property=MemoryMax=2G',
'--property=PrivateNetwork=yes','--property=ProtectSystem=strict','--property=ProtectHome=read-only',
'--property=PrivateTmp=yes','--property=ReadWritePaths='+str(work),
'--property=StandardOutput=file:'+str(work/'build.stdout'),'--property=StandardError=file:'+str(work/'build.stderr'),
'--setenv=PATH=/usr/bin:/bin','--setenv=GOTOOLCHAIN=local','--setenv=GOPROXY=off','--setenv=GOSUMDB=off',
'--setenv=GOMODCACHE=/root/go/pkg/mod','--setenv=GOCACHE='+str(work/'cache'),'--setenv=TMPDIR='+str(work/'tmp'),
'--setenv=GIT_OPTIONAL_LOCKS=0']+args
r=subprocess.run(command,stdin=subprocess.DEVNULL,capture_output=True,text=True,timeout=1200)
report['buildExitCode']=r.returncode
after,_=fingerprint();report['inputFingerprintAfter']=after
if before!=after:report['status']='BLOCKED_SOURCE_CHANGED_DURING_BUILD'
elif r.returncode!=0:report['status']='OFFLINE_REPRO_BUILD_FAILED'
elif (work/'gitea').is_file():
report['reproducedSha256']=sha(work/'gitea')
report['matchesRunningBinary']=report['reproducedSha256']==sha(binary)
report['status']='REPRODUCED_MATCH' if report['matchesRunningBinary'] else 'REPRODUCED_DIFFERENT_HASH'
else:report['status']='OFFLINE_REPRO_OUTPUT_MISSING'
report['privateWorkspace']=str(work);report['networkIsolated']=True;report['liveSourceReadOnly']=True
(work/'receipt.json').write_text(json.dumps(report,indent=2)+'\n')
emit()
PY
shell: bash
timeout-minutes: "25"
permissions:
contents: read
... |