| workflow_payload |
name: Runner maintenance inventory (read only)
name: Runner maintenance inventory (read only)
"on":
push:
branches: [codex/runner-maintenance-diagnostic-20261101]
paths: [.gitea/workflows/runner-maintenance-inventory.yml]
workflow_dispatch:
jobs:
inventory:
name: Read existing Gitea and runner service layout
runs-on: ubuntu-latest
if: github.repository == 'luoanwu/enterprise-platform'
steps:
- name: Read whitelisted deployment metadata without secret output
run: |
set -eu
node <<'NODE'
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { spawnSync } = require('node:child_process');
if (process.getuid?.() !== 0 || os.hostname() !== 'iZ2zed3pt2nygf9dy1a9d2Z') throw new Error('EXPECTED_RUNNER_HOST_IDENTITY_REQUIRED');
const commands = [];
function run(command, args) {
const allowed = ['systemctl','sha256sum','df','ps','go','git','getent'];
const versionOnly = path.isAbsolute(command) && args.length === 1 && args[0] === '--version';
if (!allowed.includes(command) && !versionOnly) throw new Error('READ_ONLY_COMMAND_NOT_ALLOWED');
const r = spawnSync(command, args, { encoding:'utf8', timeout:8000, maxBuffer:256*1024, input:'' });
// Never log argv/stdout: startup definitions may contain sensitive values.
commands.push({ command: path.isAbsolute(command) ? path.basename(command) : command, exitCode:r.status, errorCode:r.error?.code ?? null });
return { ok:r.status === 0, stdout:r.stdout ?? '' };
}
function publicPath(p) {
return typeof p === 'string' && p.length <= 400 && !/[\r\n]/.test(p) && !/secret|credential|token|password|private/i.test(p) ? p : 'REDACTED_OR_INVALID_PATH';
}
function meta(p) {
try { const s=fs.statSync(p);return {path:publicPath(p),exists:true,uid:s.uid,gid:s.gid,mode:(s.mode&0o777).toString(8),bytes:s.size,directory:s.isDirectory()}; }
catch { return {path:publicPath(p),exists:false}; }
}
function link(p) { try{return fs.readlinkSync(p);}catch{return null;} }
function splitArgs(s) { return (s.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? []).map(x=>x.replace(/^(['"])(.*)\1$/,'$2')); }
function flag(args,names) {
for(let i=0;i<args.length;i++) for(const n of names) {
if(args[i]===n) return args[i+1] ?? null;
if(args[i].startsWith(n+'=')) return args[i].slice(n.length+1);
}
return null;
}
const report={kind:'RunnerMaintenanceServiceInventory',schemaVersion:1,observedAtUTC:new Date().toISOString(),
releaseEligible:false,ci:{sourceSha:process.env.GITHUB_SHA??null,runId:process.env.GITHUB_RUN_ID??null},
hostname:os.hostname(),services:[],configs:[],paths:[],sourceTrees:[],tools:{},commands};
const units=['gitea.service','gitea-runner.service','gitea-ext.service','gitea-webapp.service','gitea-webhook.service'];
let giteaArgs=[],giteaBase=null,giteaConfig=null,runnerDir=null;
for(const unit of units) {
const r=run('systemctl',['show',unit,'--no-pager','--property=User,WorkingDirectory,ExecStart,MainPID,FragmentPath,RootDirectory,RootImage']);
if(!r.ok){report.services.push({unit,querySucceeded:false});continue;}
const p={};
for(const l of r.stdout.split('\n')) {const i=l.indexOf('=');if(i>=0)p[l.slice(0,i)]=l.slice(i+1);}
const start=p.ExecStart??'';
const executable=/\bpath=([^ ;]+)\s*;/.exec(start)?.[1] ?? null;
const argv=/argv\[\]=(.*?)\s*;\s*ignore_errors=/s.exec(start)?.[1] ?? '';
const args=splitArgs(argv);
const pid=/^[1-9][0-9]*$/.test(p.MainPID??'')?p.MainPID:null;
const actualExe=pid?link('/proc/'+pid+'/exe'):null;
const actualCwd=pid?link('/proc/'+pid+'/cwd'):null;
const item={unit,user:p.User??null,workingDirectory:publicPath(p.WorkingDirectory??''),
unitFile:publicPath(p.FragmentPath??''),rootDirectory:publicPath(p.RootDirectory??''),
rootImage:publicPath(p.RootImage??''),executable:executable?publicPath(executable):null,
processExecutable:actualExe?publicPath(actualExe):null,processWorkingDirectory:actualCwd?publicPath(actualCwd):null,
scripts:args.filter(x=>path.isAbsolute(x)&&/\.(?:py|js|mjs|cjs|sh)$/.test(x)).map(publicPath),
configFlag:flag(args,['--config','-c'])?publicPath(flag(args,['--config','-c'])):null};
if(unit==='gitea.service'||unit==='gitea-runner.service') {
const binary=actualExe && !actualExe.endsWith(' (deleted)')?actualExe:executable;
if(binary&&path.isAbsolute(binary)&&fs.existsSync(binary)){
const sum=run('sha256sum',[binary]);item.binarySha256=sum.ok?sum.stdout.match(/^[a-f0-9]{64}/)?.[0]??null:null;
const version=run(binary,['--version']);item.version=version.ok?version.stdout.trim().slice(0,400):null;
const go=run('go',['version','-m',binary]);
item.goBuild=go.ok?go.stdout.split('\n').map(l=>l.trim()).filter(l=>/^(?:path|mod)\s+(?:code\.gitea\.io|github\.com\/go-gitea|gitea\.com\/gitea)/.test(l)||/^build\s+vcs(?:\.revision|\.time|\.modified)?=/.test(l)):null;
}
}
report.services.push(item);
if(unit==='gitea.service'){
giteaArgs=args;giteaBase=flag(args,['--work-path','-w'])||actualCwd||p.WorkingDirectory||path.dirname(executable??'/');
giteaConfig=flag(args,['--config','-c']);
}
if(unit==='gitea-runner.service')runnerDir=actualCwd||p.WorkingDirectory;
}
if(giteaConfig && !path.isAbsolute(giteaConfig))giteaConfig=path.resolve(giteaBase,giteaConfig);
const configCandidates=[giteaConfig,path.join(giteaBase||'/nonexistent','custom/conf/app.ini'),
'/etc/gitea/app.ini','/home/git/gitea/custom/conf/app.ini','/home/git/gitea/conf/app.ini'].filter(Boolean);
const safeKeys={
database:['DB_TYPE','HOST','NAME','USER','PATH','SCHEMA','SSL_MODE','CHARSET'],
server:['APP_DATA_PATH','ROOT_URL','DOMAIN','HTTP_ADDR','HTTP_PORT','SSH_DOMAIN','SSH_PORT','START_SSH_SERVER','LOCAL_ROOT_URL'],
repository:['ROOT'],actions:['ENABLED','DEFAULT_ACTIONS_URL','MAX_RERUN_ATTEMPTS','ARTIFACT_RETENTION_DAYS'],
log:['MODE','ROOT_PATH'],
};
const storageKeys=['STORAGE_TYPE','PATH','MINIO_ENDPOINT','MINIO_BUCKET','MINIO_BASE_PATH','MINIO_LOCATION','MINIO_USE_SSL','SERVE_DIRECT'];
const configPaths=new Set();
for(const p of [...new Set(configCandidates)]) {
report.paths.push(meta(p));
if(!fs.existsSync(p)||!fs.statSync(p).isFile()||fs.statSync(p).size>1024*1024)continue;
if(/\/(?:\.secrets|\.evidence-private)\//.test(p))throw new Error('RESTRICTED_PATH_REFUSED');
const ini=fs.readFileSync(p,'utf8');let section='DEFAULT';const selected={};
for(const line of ini.split('\n')) {
const s=line.trim();if(!s||s.startsWith(';')||s.startsWith('#'))continue;
const h=/^\[([^\]]+)\]$/.exec(s);if(h){section=h[1].toLowerCase();continue;}
const m=/^([A-Za-z0-9_]+)\s*=\s*(.*)$/.exec(s);if(!m)continue;
const key=m[1].toUpperCase();
const storage=/^(?:storage(?:\.|$)|lfs$|attachment$|packages$)/.test(section);
if(!(safeKeys[section]??[]).includes(key)&&!(storage&&storageKeys.includes(key)))continue;
let value=m[2].trim().replace(/^(['"])(.*)\1$/,'$2');
if(/PASSW|TOKEN|SECRET|CREDENTIAL|PRIVATE_KEY/i.test(value)){value='REDACTED_SENSITIVE_REFERENCE';}
else if(value.includes('@')){value=value.replace(/[^/@\s]+@/g,'REDACTED@');}
(selected[section]??={})[key]=value.slice(0,500);
if((key==='PATH'||key==='ROOT'||key==='APP_DATA_PATH'||key==='ROOT_PATH')&&path.isAbsolute(value))configPaths.add(value);
}
const sum=run('sha256sum',[p]);
report.configs.push({path:publicPath(p),sha256:sum.ok?sum.stdout.match(/^[a-f0-9]{64}/)?.[0]??null:null,selectedNonSecretFields:selected});
}
for(const p of configPaths)report.paths.push(meta(p));
if(runnerDir){report.runnerWorkingDirectory=publicPath(runnerDir);report.runnerRegistrationMetadataOnly=meta(path.join(runnerDir,'.runner'));}
for(const dir of [...new Set([giteaBase,runnerDir].filter(Boolean))]){
const git=run('git',['-C',dir,'rev-parse','--show-toplevel']);
if(!git.ok)continue;
const root=git.stdout.trim();const head=run('git',['-C',root,'rev-parse','HEAD']);
const status=run('git',['--no-optional-locks','-C',root,'status','--porcelain','--untracked-files=no']);
report.sourceTrees.push({root:publicPath(root),head:head.ok?head.stdout.trim():null,trackedDirty:status.ok?status.stdout.trim().length>0:null});
}
const processes=run('ps',['-eo','pid=,comm=']);
report.relevantProcessNames=processes.ok?processes.stdout.split('\n').map(l=>l.trim().split(/\s+/)).filter(a=>/^(?:postgres|mysqld|mariadbd|sqlite3|gitea|act_runner|gitea-runner|nginx)$/.test(a[1]??'')).map(a=>({pid:Number(a[0]),command:a[1]})).slice(0,30):[];
const disk=run('df',['-Pk','/']);
report.rootFilesystem=disk.ok?disk.stdout.trim().split('\n').slice(-1)[0].trim().split(/\s+/).slice(1,6):null;
for(const tool of ['pg_dump','pg_restore','mysqldump','mariadb-dump','sqlite3','python3','tar','unzip','curl','git','go']){
report.tools[tool]=['/usr/bin/','/usr/local/bin/','/bin/'].map(p=>p+tool).find(p=>fs.existsSync(p))??null;
}
report.boundaries={configBodyPrinted:false,secretValuesPrinted:false,environmentRead:false,registrationBodyRead:false,serviceChanges:false,backupsOrUpgrades:false};
report.conclusion='READ_ONLY_INVENTORY_ONLY: target/storage/backup-restore and fixed-version compatibility still require verification before maintenance';
console.log('RUNNER_MAINTENANCE_INVENTORY_JSON '+JSON.stringify(report));
NODE
shell: bash
timeout-minutes: "4"
permissions:
contents: read
... |