docs: preserve reviewed NetKingdom session artifacts
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Reject generated-only commits / generated-commit-guard (push) Waiting to run

This commit is contained in:
codex 2026-11-01 23:20:36 +01:00
parent cb60cd3bbe
commit 109a90a424
50 changed files with 8970 additions and 0 deletions

View file

@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import time
OWNER=Path(__file__).resolve().parents[1]
INPUTS=OWNER/'build/forgejo'
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
def digest(path):
h=hashlib.sha256()
with Path(path).open('rb') as f:
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
return h.hexdigest()
def save(path,data):
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
def check(path,expected):
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
def base_env(work):
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
def sandbox(work,env):
cache=Path(env['GOMODCACHE'])
return ['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
def run(work,name,args,env=None,offline=False,cwd=None):
actual=sandbox(work,env)+args if offline else args
start=time.time()
with (work/(name+'.log')).open('w') as out:
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
code=proc.wait()
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
print(json.dumps(result),flush=True)
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
return result
def checked_inputs(work):
for n,h in LOCK['host_tools'].items():check(n,h)
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
def prepare(a,work):
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
if not getattr(a,n):raise ValueError('explicit '+n+' required')
check(getattr(a,n),LOCK[key])
if not a.module_cache:raise ValueError('explicit public module cache required')
cache=Path(a.module_cache).resolve();assert cache.is_dir()
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
for n,h in LOCK['host_tools'].items():check(n,h)
work.mkdir(parents=True);(work/'home').mkdir()
with tarfile.open(a.source_archive) as t:
# The official archive is digest-bound before any member is extracted.
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
for n in ['go_archive','node_archive']:
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
checked_inputs(work)
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
for name,package in packages.items():
if not name:continue
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
raise ValueError('non-public/non-integrity-bound npm package: '+name)
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
def normalize(folder):
epoch=LOCK['source_date_epoch']
for root,dirs,files in os.walk(folder):
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
for n in files:
p=Path(root)/n
if not p.is_symlink():os.utime(p,(epoch,epoch))
os.utime(root,(epoch,epoch))
def build(work):
checked_inputs(work);env=base_env(work)
run(work,'verify-go-modules',['go','mod','verify'],env,True)
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
normalize(work/'source')
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
run(work,'frontend',['make']+version+['frontend'],env,True)
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
checked_inputs(work)
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
def verify(work):
checked_inputs(work);binary=work/'source/gitea'
target=work/'embedded-extracted'
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
target.mkdir()
# No source, asset tree, host /usr, libraries, configuration or HOME is
# exposed to the executable. A filesystem fallback cannot pass this proof.
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
bare+=['--setenv',k,v]
bare+=['--','/app/forgejo']
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
run(work,'binary-version',bare+['--version'],outer)
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
records={}
for folder in ['public','options','templates']:
for f in sorted((work/'source'/folder).rglob('*')):
if not f.is_file():continue
n=str(f.relative_to(work/'source'));q=target/n
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
records[n]=digest(f)
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
assert 'public/assets/licenses.txt' in records
save(work/'embedded-files.json',records)
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
assert 'INTERP' not in readelf.stdout
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
'no_source_assets_libraries_or_host_config_mounted':True,
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
print((work/'verification.json').read_text())
def container(work):
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
context=work/'container-context'
if context.exists():raise ValueError('container trial already exists; retain evidence')
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
config=work/'docker-config';config.mkdir(exist_ok=True)
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
run(work,'container-build',['docker','buildx','build','--load','--platform',LOCK['platform'],'--pull=false',
'--network=none','--provenance=false','--sbom=false','--iidfile',str(work/'image-id'),
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(LOCK['source_date_epoch']),str(context)],env)
export_container(work)
def export_container(work):
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
check(work/'container-context/gitea',verified['binary_sha256'])
assert json.loads((work/'container-build-result.json').read_text())['returncode']==0
metadata=json.loads((work/'image-build-metadata.json').read_text())
config_digest=(work/'image-id').read_text().strip()
assert config_digest==metadata['containerimage.config.digest']
image=metadata['containerimage.digest'];assert image.startswith('sha256:')
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(work/'docker-config'),'DOCKER_HOST':'unix:///var/run/docker.sock'}
# Docker's containerd store addresses an untagged image by manifest digest;
# classic stores address it by config digest. Keep both identities explicit.
reference=None
for candidate in [image,config_digest]:
r=subprocess.run(['docker','image','inspect','--format','{{.Id}}',candidate],env=env,capture_output=True,text=True)
if r.returncode==0 and r.stdout.strip() in (image,config_digest):reference=candidate;break
if reference is None:raise RuntimeError('exact built local image cannot be resolved; never rebuild blindly')
archive_path=work/'image.tar'
if archive_path.exists():raise ValueError('retain existing export before explicit retry')
run(work,'container-save',['docker','image','save','--output',str(archive_path),reference],env)
with tarfile.open(work/'image.tar') as archive:
manifest=json.load(archive.extractfile('manifest.json'));assert len(manifest)==1
item=manifest[0];config_bytes=archive.extractfile(item['Config']).read()
assert 'sha256:'+hashlib.sha256(config_bytes).hexdigest()==config_digest
config=json.loads(config_bytes)
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes();assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
original=json.loads(base_bytes)
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
assert config['config'].get(key)==original['config'].get(key),key
assert config['config']['User']=='1000:1000'
found=False
with tarfile.open(fileobj=archive.extractfile(item['Layers'][-1]),mode='r|*') as layer:
for member in layer:
if member.name.lstrip('./')=='app/gitea/gitea':
h=hashlib.sha256();stream=layer.extractfile(member)
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
assert h.hexdigest()==verified['binary_sha256'];found=True
assert found
save(work/'container-result.json',{'local_image_id':image,'config_digest':config_digest,'local_export_reference':reference,'archive_sha256':digest(work/'image.tar'),
'all_base_layers_and_rootless_runtime_config_preserved':True,'final_layer_binary_exact':True,
'registry_publication':False,'deployment_tag':None,'host_or_secret_mounts':False,'privileged_containers':False})
print((work/'container-result.json').read_text())
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container','container-export'])
parser.add_argument('--workspace',required=True)
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
a=parser.parse_args();work=Path(a.workspace).resolve()
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
if a.phase=='prepare':return prepare(a,work)
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
if a.phase=='acquire-npm':
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
{'build':build,'verify':verify,'container':container,'container-export':export_container}[a.phase](work)
if __name__=='__main__':main()

View file

@ -0,0 +1,19 @@
import importlib.util,pathlib,json,hashlib,shutil,sys
packet=pathlib.Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1')
spec=importlib.util.spec_from_file_location('release',packet/'owner-source/tools/forgejo-release-build.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
which=sys.argv[1]
work=pathlib.Path('/tmp/apps-forgejo-release-repro-r1' if which=='second' else '/tmp/apps-forgejo-release-build-r1')
predecessor=m.digest(work/'source/gitea');shutil.copyfile(work/'source/gitea',work/'forgejo-before-directory-normalization')
m.checked_inputs(work)
for n in ['public','options','templates','modules/migration/schemas']:m.normalize(work/'source'/n)
args=['make','GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+m.LOCK['release_version'],'FORGEJO_VERSION='+m.LOCK['release_version'],'FORGEJO_VERSION_API='+m.LOCK['release_version'],'TAGS='+m.LOCK['tags'],'go-check','generate-backend','static-executable']
if which=='second':
env=m.base_env(work);m.run(work,'normalized-embedded-and-static-backend',args,env,True)
else:
env=json.loads((work/'build-env.json').read_text());env['GOCACHE']='/tmp/apps-forgejo-release-repro-r1/gocache'
reviewer=pathlib.Path('/tmp/forgejo-independent-r1')
cmd=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(work),str(work),'--bind',env['GOCACHE'],env['GOCACHE'],'--ro-bind',str(reviewer/'go'),str(reviewer/'go'),'--ro-bind',str(reviewer/'gomodcache'),str(reviewer/'gomodcache'),'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]+args
m.run(work,'normalized-embedded-and-static-backend',cmd,{'PATH':'/usr/bin:/bin','HOME':str(work/'home')})
m.checked_inputs(work)
m.save(work/'normalized-build.json',{'binary_sha256':m.digest(work/'source/gitea'),'predecessor_sha256':predecessor,'file_and_directory_times_normalized':True,'explicit_make_GOFLAGS':True,'source_locks_preserved':True,'refinement_same_trial':True,'owner_approval':False})
print((work/'normalized-build.json').read_text())

View file

@ -0,0 +1,71 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:bc5a3df111729582faadebd01d126ba769e2b65d0c62f306f6bc79f4e562452b",
"size": 7360
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:f18232174bc91741fdf3da96d85011092101a032a93a388b79e99e69c2d5c870",
"size": 3642247
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ee6a63471a2d8c6f987ad8699af167dc045c893d6060441f6f7aa216edf48b5b",
"size": 16935733
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:6dea1fa90bf9ce85ca9119fea80966f08328f12d1a7f78e01f1ec22b7bdf71a7",
"size": 907
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:9308091ee87a5af192b263f4e48a2b74532fc6173959cf815880960595f35f6d",
"size": 162
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:b2912c13f9ccbba1aa1dde241cf7db994fb5622a110d875cf20f8b33246aebc8",
"size": 170
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:3448a9fe7124e7ce78e2ba1c320f6149ecec4e1daa7a043b88e9dd3b7681d5e3",
"size": 2349
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:07f673aaf066a1cec01ab6768992ee260e4794f693cb22585d2e7d7386387faf",
"size": 169
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:a3ee7de65d57741a38da7c9852b81ed9a57b848c68bc5b03d07b6c07a4802c20",
"size": 45351336
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:047a4c09ff1e32c905762257e4c9b58eef86fc7d126e44e45ced4d266612505e",
"size": 154
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ac2d856a9d2af551ff928db4d466771d9de670c7affd3f22e20e2b4b9db5db6b",
"size": 7036678
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:b9fa35057fd93015c8ec114eb39686e071fdccce318be5eff55b2cae165a2ff0",
"size": 550
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
"size": 32
}
]
}

View file

@ -0,0 +1,73 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b",
"size": 2563,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:5b8afd5c19392bf83b0bf2761eb03c214b98851e6460d99597bf133f81408522",
"size": 2563,
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:476c51f625f2e42b604132cc1dc5756fd2391754ffacc93a4a330ff5da8ca213",
"size": 2563,
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v6"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:b7c7a4727334c8a16db692e17c4d9f1344dd08ad745d3047ce1e29e975b0dbb5",
"size": 566,
"annotations": {
"vnd.docker.reference.digest": "sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:e1709efc46fe4720bb26c37eea9ba21ebb02db826d0192a3c8f2dc717c116057",
"size": 566,
"annotations": {
"vnd.docker.reference.digest": "sha256:5b8afd5c19392bf83b0bf2761eb03c214b98851e6460d99597bf133f81408522",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:7d5556ea38cca9d296fba95aa43ccafe5d74c23252dad69369ae6f9d88fbd9e4",
"size": 566,
"annotations": {
"vnd.docker.reference.digest": "sha256:476c51f625f2e42b604132cc1dc5756fd2391754ffacc93a4a330ff5da8ca213",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}

View file

@ -0,0 +1,9 @@
{
"go": "go version go1.24.3 linux/amd64",
"node": "v22.15.0",
"gcc": "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0",
"make": "GNU Make 4.3",
"npm": "10.9.2",
"platform": "linux/amd64",
"empty_HOME_and_closed_environment": true
}

View file

@ -0,0 +1,8 @@
{
"failed_phase": "embedded extraction harness",
"reason": "upstream CLI requires explicit file pattern; omitted glob",
"source_binary_unchanged": true,
"correction": "supply literal ** argv, with no shell expansion",
"failed_log_and_result_retained": true,
"same_trial": true
}

View file

@ -0,0 +1,13 @@
{
"all_public_options_templates_exact": true,
"architecture_approval": false,
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
"embedded_files": 2234,
"native_admission": false,
"network": "bwrap --unshare-net",
"new_cli_flag_present": true,
"no_dynamic_interpreter": true,
"no_source_assets_libraries_or_host_config_mounted": true,
"source_locks_preserved": true
}

View file

@ -0,0 +1,10 @@
{
"argv": [
"go",
"mod",
"verify"
],
"phase": "verify-go-modules",
"returncode": 0,
"seconds": 11.88
}

View file

@ -0,0 +1,684 @@
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
VERGABE_RELEASE ?= vergabe-teilnahme
VERGABE_NAMESPACE ?= vergabe-teilnahme
VERGABE_CHART ?= charts/vergabe-teilnahme
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
VERGABE_IMAGE_TAG ?=
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
VERGABE_DB_SECRET ?= vergabe-app-credentials
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
VERGABE_DB_USER ?= vergabe
VERGABE_DB_HOST ?= apps-pg-rw.databases
VERGABE_DB_PORT ?= 5432
VERGABE_DB_NAME ?= vergabe_db
COULOMB_SOCIAL_RELEASE ?= coulomb-social
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
INTER_HUB_RELEASE ?= inter-hub
INTER_HUB_NAMESPACE ?= inter-hub
INTER_HUB_CHART ?= charts/inter-hub
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
INTER_HUB_IMAGE_TAG ?=
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
REUSE_RELEASE ?= reuse
REUSE_NAMESPACE ?= reuse
REUSE_CHART ?= charts/reuse-surface
REUSE_VALUES ?= helm/reuse-surface-values.yaml
REUSE_URL ?= https://reuse.coulomb.social
REUSE_CERTIFICATE ?= reuse-surface-tls
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
POLICY_NEXUS_TARGET ?= railiance01
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
POLICY_NEXUS_REVISION ?=
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_RELEASE ?= forgejo
FORGEJO_NAMESPACE ?= forgejo
FORGEJO_CHART ?= gitea-charts/gitea
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
FORGEJO_CHART_VERSION ?= 12.5.0
FORGEJO_VALUES ?= helm/forgejo-values.yaml
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
FORGEJO_DB_CLUSTER ?= forgejo-db
FORGEJO_DB_NAMESPACE ?= databases
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
PLATFORM_REPO ?= $(HOME)/railiance-platform
ESO_CHART_VERSION ?= 0.16.1
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
CORE_HUB_RELEASE ?= core-hub
CORE_HUB_NAMESPACE ?= core-hub
CORE_HUB_CHART ?= charts/core-hub
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
CORE_HUB_IMAGE_TAG ?=
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
DRY_RUN_CREATE_NAMESPACES ?= false
##@ Operator checks
check-tools: ## Check required operator tools and warn about optional diagnostics
tools/check-tools.sh
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
apps-pg-status: ## Check the shared apps-pg cnpg cluster
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
kubectl cnpg status apps-pg -n databases; \
else \
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
kubectl get cluster apps-pg -n databases; \
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
fi
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
echo "Restore it with:" >&2; \
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
exit 1; \
}
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
echo "Run: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
exit 1; \
}
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
exit 1; \
}
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
chmod +x tools/cnpg-logical-backup.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/cnpg-logical-backup.sh
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
chmod +x tools/cnpg-logical-backup.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/cnpg-logical-backup.sh
##@ Option A multi-host (platform CLI / activity-core)
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
exit 1; \
}
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
chmod +x tools/apps-pg-backup-dry-run.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/apps-pg-backup-dry-run.sh
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
chmod +x tools/apps-pg-restore-drill.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
chmod +x tools/apps-pg-backup-dry-run.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/apps-pg-backup-dry-run.sh
##@ Vergabe Teilnahme
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
@test -n "$(VERGABE_IMAGE_TAG)" || { \
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
exit 1; \
}
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) --create-namespace \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
kubectl apply -f $(VERGABE_INGRESS)
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
vergabe-migrate: ## Run Django migrations against the live deployment
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
vergabe-seed: ## Run the idempotent seed command
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
vergabe-superuser: ## Open an interactive shell for createsuperuser
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
vergabe-logs: ## Tail vergabe-teilnahme app logs
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
APP_DB_USER=$(VERGABE_DB_USER) \
APP_DB_HOST=$(VERGABE_DB_HOST) \
APP_DB_PORT=$(VERGABE_DB_PORT) \
APP_DB_NAME=$(VERGABE_DB_NAME) \
tools/build-database-url-secret.sh
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
exit 1)
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE)
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
--wait --timeout 5m
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
coulomb-social-logs: ## Tail coulomb-social app logs
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
##@ Inter-Hub
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES)
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
##@ Core Hub
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
chmod +x tools/core-hub-helm-adopt.sh
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
tools/core-hub-helm-adopt.sh
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES)
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
##@ Forgejo (forgejo.coulomb.social)
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
--version $(ESO_CHART_VERSION) \
--namespace external-secrets --create-namespace \
--set installCRDs=true \
--set serviceAccount.create=true \
--set serviceAccount.name=external-secrets \
--wait --timeout 5m
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
tools/forgejo-openbao-eso-token-apply.sh
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--dry-run=server --timeout 5m
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--wait --timeout 10m
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
else \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
fi
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
tools/forgejo-npm-smoke.sh
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
else \
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
fi
@echo "ok: Forgejo SOPS sentinels"
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
##@ reuse-surface (reuse.coulomb.social)
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) \
-f $(REUSE_VALUES)
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) --create-namespace \
-f $(REUSE_VALUES) --wait --timeout 5m
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
curl -fsS "$(REUSE_URL)/health"
curl -fsS "$(REUSE_URL)/v1/repos"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
bash tools/reuse-forgejo-webhook.sh
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
bash tools/reuse-webhook-smoke.sh
##@ policy-nexus (policy.coulomb.social)
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-status: ## Show the policy-nexus Helm release state
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-rollback: ## Roll back to a named previous Helm revision
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
##@ Help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
.PHONY: forgejo-secret-scan-template-deploy
##@ Isolated Forgejo source release preparation (no deployment/publication)
FORGEJO_BUILD_WORKSPACE ?= /tmp/railiance-apps-forgejo-release
FORGEJO_SOURCE_ARCHIVE ?=
FORGEJO_GO_ARCHIVE ?=
FORGEJO_NODE_ARCHIVE ?=
FORGEJO_BUILD_GOMODCACHE ?=
FORGEJO_BUILD_GOCACHE_SEED ?=
FORGEJO_BUILD_NPM_CACHE_SEED ?=
.PHONY: forgejo-release-prepare forgejo-release-acquire-npm forgejo-release-build forgejo-release-verify forgejo-release-container
forgejo-release-prepare: ## Verify archives/patch/locks and prepare a new isolated workspace
/usr/bin/python3 -I tools/forgejo-release-build.py prepare --workspace "$(FORGEJO_BUILD_WORKSPACE)" --source-archive "$(FORGEJO_SOURCE_ARCHIVE)" --go-archive "$(FORGEJO_GO_ARCHIVE)" --node-archive "$(FORGEJO_NODE_ARCHIVE)" --module-cache "$(FORGEJO_BUILD_GOMODCACHE)" $(if $(FORGEJO_BUILD_GOCACHE_SEED),--go-cache-seed "$(FORGEJO_BUILD_GOCACHE_SEED)") $(if $(FORGEJO_BUILD_NPM_CACHE_SEED),--npm-cache-seed "$(FORGEJO_BUILD_NPM_CACHE_SEED)")
forgejo-release-acquire-npm: ## Explicit public registry dependency acquisition; ignores lifecycle scripts
/usr/bin/python3 -I tools/forgejo-release-build.py acquire-npm --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-build: ## Offline full frontend, bindata and static backend via upstream targets
/usr/bin/python3 -I tools/forgejo-release-build.py build --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-verify: ## Verify all embedded assets, static executable and new CLI shape offline
/usr/bin/python3 -I tools/forgejo-release-build.py verify --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-container: ## Build/verify a deterministic local rootless OCI archive; never publish or deploy
/usr/bin/python3 -I tools/forgejo-release-build.py container --workspace "$(FORGEJO_BUILD_WORKSPACE)"

View file

@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import time
import math
import datetime
OWNER=Path(__file__).resolve().parents[1]
INPUTS=OWNER/'build/forgejo'
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
def digest(path):
h=hashlib.sha256()
with Path(path).open('rb') as f:
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
return h.hexdigest()
def save(path,data):
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
def check(path,expected):
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
def base_env(work):
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
def sandbox(work,env):
cache=Path(env['GOMODCACHE'])
return ['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
def run(work,name,args,env=None,offline=False,cwd=None):
actual=sandbox(work,env)+args if offline else args
start=time.time()
with (work/(name+'.log')).open('w') as out:
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
code=proc.wait()
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
print(json.dumps(result),flush=True)
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
return result
def checked_inputs(work):
for n,h in LOCK['host_tools'].items():check(n,h)
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
def prepare(a,work):
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
if not getattr(a,n):raise ValueError('explicit '+n+' required')
check(getattr(a,n),LOCK[key])
if not a.module_cache:raise ValueError('explicit public module cache required')
cache=Path(a.module_cache).resolve();assert cache.is_dir()
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
for n,h in LOCK['host_tools'].items():check(n,h)
work.mkdir(parents=True);(work/'home').mkdir()
with tarfile.open(a.source_archive) as t:
# The official archive is digest-bound before any member is extracted.
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
for n in ['go_archive','node_archive']:
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
checked_inputs(work)
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
for name,package in packages.items():
if not name:continue
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
raise ValueError('non-public/non-integrity-bound npm package: '+name)
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
def normalize(folder):
epoch=LOCK['source_date_epoch']
for root,dirs,files in os.walk(folder):
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
for n in files:
p=Path(root)/n
if not p.is_symlink():os.utime(p,(epoch,epoch))
os.utime(root,(epoch,epoch))
def build(work):
checked_inputs(work);env=base_env(work)
run(work,'verify-go-modules',['go','mod','verify'],env,True)
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
normalize(work/'source')
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
run(work,'frontend',['make']+version+['frontend'],env,True)
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
checked_inputs(work)
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
def verify(work):
checked_inputs(work);binary=work/'source/gitea'
target=work/'embedded-extracted'
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
target.mkdir()
# No source, asset tree, host /usr, libraries, configuration or HOME is
# exposed to the executable. A filesystem fallback cannot pass this proof.
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
bare+=['--setenv',k,v]
bare+=['--','/app/forgejo']
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
run(work,'binary-version',bare+['--version'],outer)
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
records={}
for folder in ['public','options','templates']:
for f in sorted((work/'source'/folder).rglob('*')):
if not f.is_file():continue
n=str(f.relative_to(work/'source'));q=target/n
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
records[n]=digest(f)
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
assert 'public/assets/licenses.txt' in records
save(work/'embedded-files.json',records)
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
assert 'INTERP' not in readelf.stdout
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
'no_source_assets_libraries_or_host_config_mounted':True,
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
print((work/'verification.json').read_text())
def container(work):
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
context=work/'container-context'
if context.exists():raise ValueError('container trial already exists; retain evidence before an explicit revised trial')
archive_path=work/'image.oci.tar'
if archive_path.exists():raise ValueError('retain existing OCI artifact; never overwrite it')
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes()
assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
original=json.loads(base_bytes)
epoch=max(LOCK['source_date_epoch'],math.ceil(datetime.datetime.fromisoformat(original['created'].replace('Z','+00:00')).timestamp()))
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
config=work/'docker-config';config.mkdir(exist_ok=True)
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
# SOURCE_DATE_EPOCH alone does not normalise COPY parent-directory mtimes.
# OCI output permits layer rewriting without the Docker-store unpack conflict.
# The epoch never predates the immutable base, preserving its exact layer prefix.
run(work,'container-build',['docker','buildx','build','--output','type=oci,dest='+str(archive_path)+',rewrite-timestamp=true',
'--no-cache','--platform',LOCK['platform'],'--pull=false','--network=none','--provenance=false','--sbom=false',
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(epoch),str(context)],env)
metadata=json.loads((work/'image-build-metadata.json').read_text())
with tarfile.open(archive_path) as archive:
index_bytes=archive.extractfile('index.json').read();index=json.loads(index_bytes)
assert len(index['manifests'])==1
descriptor=index['manifests'][0]
assert descriptor['platform']=={'architecture':'amd64','os':'linux'}
assert descriptor['digest']==metadata['containerimage.digest']
def blob(d):
value=archive.extractfile('blobs/sha256/'+d.split(':')[1]).read()
assert 'sha256:'+hashlib.sha256(value).hexdigest()==d
return value
manifest=json.loads(blob(descriptor['digest']))
config_digest=manifest['config']['digest'];config=json.loads(blob(config_digest))
assert config_digest==metadata['containerimage.config.digest']
for layer in manifest['layers']:blob(layer['digest'])
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
assert config['architecture']=='amd64' and config['os']=='linux'
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
assert config['config'].get(key)==original['config'].get(key),key
labels=dict(original['config'].get('Labels',{}))
labels.update({'org.opencontainers.image.version':LOCK['release_version'],
'org.opencontainers.image.revision':LOCK['source_commit'],'org.railiance.source.patch.sha256':LOCK['patch_sha256']})
assert config['config'].get('Labels')==labels
assert config['config']['User']=='1000:1000'
expected_created=datetime.datetime.fromtimestamp(epoch,datetime.timezone.utc).isoformat(timespec='seconds').replace('+00:00','Z')
assert config['created']==expected_created
found=False
last='blobs/sha256/'+manifest['layers'][-1]['digest'].split(':')[1]
with tarfile.open(fileobj=archive.extractfile(last),mode='r|*') as layer:
for member in layer:
name=member.name.lstrip('./');assert name in ('app','app/gitea','app/gitea/gitea')
if name=='app/gitea/gitea':
h=hashlib.sha256();stream=layer.extractfile(member)
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
assert h.hexdigest()==verified['binary_sha256'];found=True
assert found
save(work/'container-result.json',{'image_manifest_digest':descriptor['digest'],'config_digest':config_digest,
'oci_index_sha256':hashlib.sha256(index_bytes).hexdigest(),'archive_sha256':digest(archive_path),
'artifact_path':str(archive_path),'source_epoch':LOCK['source_date_epoch'],'container_epoch':epoch,
'all_base_layers_and_rootless_runtime_config_preserved':True,'all_oci_blob_digests_verified':True,
'final_layer_binary_exact':True,'registry_publication':False,'deployment_tag':None,
'host_or_secret_mounts':False,'privileged_containers':False,'local_runtime_image_loaded':False})
print((work/'container-result.json').read_text())
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container'])
parser.add_argument('--workspace',required=True)
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
a=parser.parse_args();work=Path(a.workspace).resolve()
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
if a.phase=='prepare':return prepare(a,work)
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
if a.phase=='acquire-npm':
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
{'build':build,'verify':verify,'container':container}[a.phase](work)
if __name__=='__main__':main()

View file

@ -0,0 +1,657 @@
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
VERGABE_RELEASE ?= vergabe-teilnahme
VERGABE_NAMESPACE ?= vergabe-teilnahme
VERGABE_CHART ?= charts/vergabe-teilnahme
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
VERGABE_IMAGE_TAG ?=
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
VERGABE_DB_SECRET ?= vergabe-app-credentials
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
VERGABE_DB_USER ?= vergabe
VERGABE_DB_HOST ?= apps-pg-rw.databases
VERGABE_DB_PORT ?= 5432
VERGABE_DB_NAME ?= vergabe_db
COULOMB_SOCIAL_RELEASE ?= coulomb-social
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
INTER_HUB_RELEASE ?= inter-hub
INTER_HUB_NAMESPACE ?= inter-hub
INTER_HUB_CHART ?= charts/inter-hub
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
INTER_HUB_IMAGE_TAG ?=
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
REUSE_RELEASE ?= reuse
REUSE_NAMESPACE ?= reuse
REUSE_CHART ?= charts/reuse-surface
REUSE_VALUES ?= helm/reuse-surface-values.yaml
REUSE_URL ?= https://reuse.coulomb.social
REUSE_CERTIFICATE ?= reuse-surface-tls
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
POLICY_NEXUS_TARGET ?= railiance01
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
POLICY_NEXUS_REVISION ?=
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_RELEASE ?= forgejo
FORGEJO_NAMESPACE ?= forgejo
FORGEJO_CHART ?= gitea-charts/gitea
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
FORGEJO_CHART_VERSION ?= 12.5.0
FORGEJO_VALUES ?= helm/forgejo-values.yaml
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
FORGEJO_DB_CLUSTER ?= forgejo-db
FORGEJO_DB_NAMESPACE ?= databases
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
PLATFORM_REPO ?= $(HOME)/railiance-platform
ESO_CHART_VERSION ?= 0.16.1
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
CORE_HUB_RELEASE ?= core-hub
CORE_HUB_NAMESPACE ?= core-hub
CORE_HUB_CHART ?= charts/core-hub
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
CORE_HUB_IMAGE_TAG ?=
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
DRY_RUN_CREATE_NAMESPACES ?= false
##@ Operator checks
check-tools: ## Check required operator tools and warn about optional diagnostics
tools/check-tools.sh
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
apps-pg-status: ## Check the shared apps-pg cnpg cluster
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
kubectl cnpg status apps-pg -n databases; \
else \
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
kubectl get cluster apps-pg -n databases; \
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
fi
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
echo "Restore it with:" >&2; \
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
exit 1; \
}
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
echo "Run: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
exit 1; \
}
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
exit 1; \
}
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
chmod +x tools/cnpg-logical-backup.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/cnpg-logical-backup.sh
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
chmod +x tools/cnpg-logical-backup.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/cnpg-logical-backup.sh
##@ Option A multi-host (platform CLI / activity-core)
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
exit 1; \
}
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
chmod +x tools/apps-pg-backup-dry-run.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/apps-pg-backup-dry-run.sh
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
chmod +x tools/apps-pg-restore-drill.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
chmod +x tools/apps-pg-backup-dry-run.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/apps-pg-backup-dry-run.sh
##@ Vergabe Teilnahme
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
@test -n "$(VERGABE_IMAGE_TAG)" || { \
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
exit 1; \
}
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) --create-namespace \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
kubectl apply -f $(VERGABE_INGRESS)
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
vergabe-migrate: ## Run Django migrations against the live deployment
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
vergabe-seed: ## Run the idempotent seed command
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
vergabe-superuser: ## Open an interactive shell for createsuperuser
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
vergabe-logs: ## Tail vergabe-teilnahme app logs
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
APP_DB_USER=$(VERGABE_DB_USER) \
APP_DB_HOST=$(VERGABE_DB_HOST) \
APP_DB_PORT=$(VERGABE_DB_PORT) \
APP_DB_NAME=$(VERGABE_DB_NAME) \
tools/build-database-url-secret.sh
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
exit 1)
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE)
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
--wait --timeout 5m
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
coulomb-social-logs: ## Tail coulomb-social app logs
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
##@ Inter-Hub
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES)
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
##@ Core Hub
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
chmod +x tools/core-hub-helm-adopt.sh
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
tools/core-hub-helm-adopt.sh
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES)
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
##@ Forgejo (forgejo.coulomb.social)
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
--version $(ESO_CHART_VERSION) \
--namespace external-secrets --create-namespace \
--set installCRDs=true \
--set serviceAccount.create=true \
--set serviceAccount.name=external-secrets \
--wait --timeout 5m
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
tools/forgejo-openbao-eso-token-apply.sh
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--dry-run=server --timeout 5m
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--wait --timeout 10m
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
else \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
fi
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
tools/forgejo-npm-smoke.sh
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
else \
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
fi
@echo "ok: Forgejo SOPS sentinels"
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
##@ reuse-surface (reuse.coulomb.social)
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) \
-f $(REUSE_VALUES)
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) --create-namespace \
-f $(REUSE_VALUES) --wait --timeout 5m
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
curl -fsS "$(REUSE_URL)/health"
curl -fsS "$(REUSE_URL)/v1/repos"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
bash tools/reuse-forgejo-webhook.sh
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
bash tools/reuse-webhook-smoke.sh
##@ policy-nexus (policy.coulomb.social)
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-status: ## Show the policy-nexus Helm release state
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-rollback: ## Roll back to a named previous Helm revision
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
##@ Help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
.PHONY: forgejo-secret-scan-template-deploy

View file

@ -0,0 +1,26 @@
{
"all_owned_exec_handles_complete": true,
"live_exec_handles": [],
"live_wait_cells": [],
"live_owned_build_processes": [],
"completed_handles": [
40476,
7562,
46393,
15845,
33305,
32691,
43522,
52717,
47031,
86788,
38632,
80551,
50174,
83373,
31050,
33100
],
"application_container_started": false,
"note": "Existing Docker daemon/build cache and retained prototype images are not live owned jobs. No restart on observation timeout."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,684 @@
SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
VERGABE_RELEASE ?= vergabe-teilnahme
VERGABE_NAMESPACE ?= vergabe-teilnahme
VERGABE_CHART ?= charts/vergabe-teilnahme
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
VERGABE_IMAGE_TAG ?=
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
VERGABE_DB_SECRET ?= vergabe-app-credentials
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
VERGABE_DB_USER ?= vergabe
VERGABE_DB_HOST ?= apps-pg-rw.databases
VERGABE_DB_PORT ?= 5432
VERGABE_DB_NAME ?= vergabe_db
COULOMB_SOCIAL_RELEASE ?= coulomb-social
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
INTER_HUB_RELEASE ?= inter-hub
INTER_HUB_NAMESPACE ?= inter-hub
INTER_HUB_CHART ?= charts/inter-hub
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
INTER_HUB_IMAGE_TAG ?=
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
REUSE_RELEASE ?= reuse
REUSE_NAMESPACE ?= reuse
REUSE_CHART ?= charts/reuse-surface
REUSE_VALUES ?= helm/reuse-surface-values.yaml
REUSE_URL ?= https://reuse.coulomb.social
REUSE_CERTIFICATE ?= reuse-surface-tls
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
POLICY_NEXUS_TARGET ?= railiance01
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
POLICY_NEXUS_REVISION ?=
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
FORGEJO_RELEASE ?= forgejo
FORGEJO_NAMESPACE ?= forgejo
FORGEJO_CHART ?= gitea-charts/gitea
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
FORGEJO_CHART_VERSION ?= 12.5.0
FORGEJO_VALUES ?= helm/forgejo-values.yaml
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
FORGEJO_DB_CLUSTER ?= forgejo-db
FORGEJO_DB_NAMESPACE ?= databases
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
PLATFORM_REPO ?= $(HOME)/railiance-platform
ESO_CHART_VERSION ?= 0.16.1
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
CORE_HUB_RELEASE ?= core-hub
CORE_HUB_NAMESPACE ?= core-hub
CORE_HUB_CHART ?= charts/core-hub
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
CORE_HUB_IMAGE_TAG ?=
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
DRY_RUN_CREATE_NAMESPACES ?= false
##@ Operator checks
check-tools: ## Check required operator tools and warn about optional diagnostics
tools/check-tools.sh
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
apps-pg-status: ## Check the shared apps-pg cnpg cluster
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
kubectl cnpg status apps-pg -n databases; \
else \
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
kubectl get cluster apps-pg -n databases; \
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
fi
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
echo "Restore it with:" >&2; \
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
exit 1; \
}
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
echo "Run: bridge up k3s-api-coulombcore" >&2; \
exit 1; \
}
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
exit 1; \
}
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
exit 1; \
}
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
chmod +x tools/cnpg-logical-backup.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/cnpg-logical-backup.sh
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
chmod +x tools/cnpg-logical-backup.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/cnpg-logical-backup.sh
##@ Option A multi-host (platform CLI / activity-core)
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
exit 1; \
}
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
chmod +x tools/apps-pg-backup-dry-run.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
tools/apps-pg-backup-dry-run.sh
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
chmod +x tools/apps-pg-restore-drill.sh
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
chmod +x tools/apps-pg-backup-dry-run.sh
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
exit 1; \
}
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
tools/apps-pg-backup-dry-run.sh
##@ Vergabe Teilnahme
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
@test -n "$(VERGABE_IMAGE_TAG)" || { \
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
exit 1; \
}
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
--namespace $(VERGABE_NAMESPACE) --create-namespace \
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
kubectl apply -f $(VERGABE_INGRESS)
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
vergabe-migrate: ## Run Django migrations against the live deployment
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
vergabe-seed: ## Run the idempotent seed command
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
vergabe-superuser: ## Open an interactive shell for createsuperuser
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
vergabe-logs: ## Tail vergabe-teilnahme app logs
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
APP_DB_USER=$(VERGABE_DB_USER) \
APP_DB_HOST=$(VERGABE_DB_HOST) \
APP_DB_PORT=$(VERGABE_DB_PORT) \
APP_DB_NAME=$(VERGABE_DB_NAME) \
tools/build-database-url-secret.sh
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
exit 1)
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE)
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
-f $(COULOMB_SOCIAL_VALUES) \
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
--wait --timeout 5m
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
coulomb-social-logs: ## Tail coulomb-social app logs
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
##@ Inter-Hub
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES)
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
##@ Core Hub
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
chmod +x tools/core-hub-helm-adopt.sh
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
tools/core-hub-helm-adopt.sh
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES)
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_NAMESPACE) \
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
-l app.kubernetes.io/name=core-hub --ignore-not-found
##@ Forgejo (forgejo.coulomb.social)
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
--version $(ESO_CHART_VERSION) \
--namespace external-secrets --create-namespace \
--set installCRDs=true \
--set serviceAccount.create=true \
--set serviceAccount.name=external-secrets \
--wait --timeout 5m
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
tools/forgejo-openbao-eso-token-apply.sh
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--dry-run=server --timeout 5m
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
--version $(FORGEJO_CHART_VERSION) \
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
-f $(FORGEJO_VALUES) \
-f $(FORGEJO_REGISTRY_VALUES) \
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
--wait --timeout 10m
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
else \
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
fi
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
tools/forgejo-npm-smoke.sh
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
else \
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
fi
@echo "ok: Forgejo SOPS sentinels"
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
exit 1; \
}
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
##@ reuse-surface (reuse.coulomb.social)
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) \
-f $(REUSE_VALUES)
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
--namespace $(REUSE_NAMESPACE) --create-namespace \
-f $(REUSE_VALUES) --wait --timeout 5m
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
curl -fsS "$(REUSE_URL)/health"
curl -fsS "$(REUSE_URL)/v1/repos"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
bash tools/reuse-forgejo-webhook.sh
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
bash tools/reuse-webhook-smoke.sh
##@ policy-nexus (policy.coulomb.social)
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-status: ## Show the policy-nexus Helm release state
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
policy-nexus-rollback: ## Roll back to a named previous Helm revision
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
##@ Help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
.PHONY: forgejo-secret-scan-template-deploy
##@ Isolated Forgejo source release preparation (no deployment/publication)
FORGEJO_BUILD_WORKSPACE ?= /tmp/railiance-apps-forgejo-release
FORGEJO_SOURCE_ARCHIVE ?=
FORGEJO_GO_ARCHIVE ?=
FORGEJO_NODE_ARCHIVE ?=
FORGEJO_BUILD_GOMODCACHE ?=
FORGEJO_BUILD_GOCACHE_SEED ?=
FORGEJO_BUILD_NPM_CACHE_SEED ?=
.PHONY: forgejo-release-prepare forgejo-release-acquire-npm forgejo-release-build forgejo-release-verify forgejo-release-container
forgejo-release-prepare: ## Verify archives/patch/locks and prepare a new isolated workspace
/usr/bin/python3 -I tools/forgejo-release-build.py prepare --workspace "$(FORGEJO_BUILD_WORKSPACE)" --source-archive "$(FORGEJO_SOURCE_ARCHIVE)" --go-archive "$(FORGEJO_GO_ARCHIVE)" --node-archive "$(FORGEJO_NODE_ARCHIVE)" --module-cache "$(FORGEJO_BUILD_GOMODCACHE)" $(if $(FORGEJO_BUILD_GOCACHE_SEED),--go-cache-seed "$(FORGEJO_BUILD_GOCACHE_SEED)") $(if $(FORGEJO_BUILD_NPM_CACHE_SEED),--npm-cache-seed "$(FORGEJO_BUILD_NPM_CACHE_SEED)")
forgejo-release-acquire-npm: ## Explicit public registry dependency acquisition; ignores lifecycle scripts
/usr/bin/python3 -I tools/forgejo-release-build.py acquire-npm --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-build: ## Offline full frontend, bindata and static backend via upstream targets
/usr/bin/python3 -I tools/forgejo-release-build.py build --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-verify: ## Verify all embedded assets, static executable and new CLI shape offline
/usr/bin/python3 -I tools/forgejo-release-build.py verify --workspace "$(FORGEJO_BUILD_WORKSPACE)"
forgejo-release-container: ## Build/verify a deterministic local rootless OCI archive; never publish or deploy
/usr/bin/python3 -I tools/forgejo-release-build.py container --workspace "$(FORGEJO_BUILD_WORKSPACE)"

View file

@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import time
import math
import datetime
OWNER=Path(__file__).resolve().parents[1]
INPUTS=OWNER/'build/forgejo'
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
def digest(path):
h=hashlib.sha256()
with Path(path).open('rb') as f:
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
return h.hexdigest()
def save(path,data):
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
def check(path,expected):
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
def base_env(work):
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
def sandbox(work,env):
cache=Path(env['GOMODCACHE'])
return ['bwrap','--unshare-net','--unshare-pid','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
def run(work,name,args,env=None,offline=False,cwd=None):
actual=sandbox(work,env)+args if offline else args
start=time.time()
with (work/(name+'.log')).open('w') as out:
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
code=proc.wait()
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
print(json.dumps(result),flush=True)
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
return result
def checked_inputs(work):
for n,h in LOCK['host_tools'].items():check(n,h)
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
def prepare(a,work):
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
if not getattr(a,n):raise ValueError('explicit '+n+' required')
check(getattr(a,n),LOCK[key])
if not a.module_cache:raise ValueError('explicit public module cache required')
cache=Path(a.module_cache).resolve();assert cache.is_dir()
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
for n,h in LOCK['host_tools'].items():check(n,h)
work.mkdir(parents=True);(work/'home').mkdir()
with tarfile.open(a.source_archive) as t:
# The official archive is digest-bound before any member is extracted.
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
for n in ['go_archive','node_archive']:
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
checked_inputs(work)
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
for name,package in packages.items():
if not name:continue
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
raise ValueError('non-public/non-integrity-bound npm package: '+name)
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
def normalize(folder):
epoch=LOCK['source_date_epoch']
for root,dirs,files in os.walk(folder):
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
for n in files:
p=Path(root)/n
if not p.is_symlink():os.utime(p,(epoch,epoch))
os.utime(root,(epoch,epoch))
def build(work):
checked_inputs(work);env=base_env(work)
run(work,'verify-go-modules',['go','mod','verify'],env,True)
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
normalize(work/'source')
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
run(work,'frontend',['make']+version+['frontend'],env,True)
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
checked_inputs(work)
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
def verify(work):
checked_inputs(work);binary=work/'source/gitea'
target=work/'embedded-extracted'
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
target.mkdir()
# No source, asset tree, host /usr, libraries, configuration or HOME is
# exposed to the executable. A filesystem fallback cannot pass this proof.
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
bare+=['--setenv',k,v]
bare+=['--','/app/forgejo']
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
run(work,'binary-version',bare+['--version'],outer)
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
records={}
for folder in ['public','options','templates']:
for f in sorted((work/'source'/folder).rglob('*')):
if not f.is_file():continue
n=str(f.relative_to(work/'source'));q=target/n
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
records[n]=digest(f)
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
assert 'public/assets/licenses.txt' in records
save(work/'embedded-files.json',records)
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
assert 'INTERP' not in readelf.stdout
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
'no_source_assets_libraries_or_host_config_mounted':True,
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
print((work/'verification.json').read_text())
def container(work):
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
context=work/'container-context'
if context.exists():raise ValueError('container trial already exists; retain evidence before an explicit revised trial')
archive_path=work/'image.oci.tar'
if archive_path.exists():raise ValueError('retain existing OCI artifact; never overwrite it')
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes()
assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
original=json.loads(base_bytes)
epoch=max(LOCK['source_date_epoch'],math.ceil(datetime.datetime.fromisoformat(original['created'].replace('Z','+00:00')).timestamp()))
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
config=work/'docker-config';config.mkdir(exist_ok=True)
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
# SOURCE_DATE_EPOCH alone does not normalise COPY parent-directory mtimes.
# OCI output permits layer rewriting without the Docker-store unpack conflict.
# The epoch never predates the immutable base, preserving its exact layer prefix.
run(work,'container-build',['docker','buildx','build','--output','type=oci,dest='+str(archive_path)+',rewrite-timestamp=true',
'--no-cache','--platform',LOCK['platform'],'--pull=false','--network=none','--provenance=false','--sbom=false',
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(epoch),str(context)],env)
metadata=json.loads((work/'image-build-metadata.json').read_text())
with tarfile.open(archive_path) as archive:
index_bytes=archive.extractfile('index.json').read();index=json.loads(index_bytes)
assert len(index['manifests'])==1
descriptor=index['manifests'][0]
assert descriptor['platform']=={'architecture':'amd64','os':'linux'}
assert descriptor['digest']==metadata['containerimage.digest']
def blob(d):
value=archive.extractfile('blobs/sha256/'+d.split(':')[1]).read()
assert 'sha256:'+hashlib.sha256(value).hexdigest()==d
return value
manifest=json.loads(blob(descriptor['digest']))
config_digest=manifest['config']['digest'];config=json.loads(blob(config_digest))
assert config_digest==metadata['containerimage.config.digest']
for layer in manifest['layers']:blob(layer['digest'])
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
assert config['architecture']=='amd64' and config['os']=='linux'
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
assert config['config'].get(key)==original['config'].get(key),key
labels=dict(original['config'].get('Labels',{}))
labels.update({'org.opencontainers.image.version':LOCK['release_version'],
'org.opencontainers.image.revision':LOCK['source_commit'],'org.railiance.source.patch.sha256':LOCK['patch_sha256']})
assert config['config'].get('Labels')==labels
assert config['config']['User']=='1000:1000'
expected_created=datetime.datetime.fromtimestamp(epoch,datetime.timezone.utc).isoformat(timespec='seconds').replace('+00:00','Z')
assert config['created']==expected_created
found=False
last='blobs/sha256/'+manifest['layers'][-1]['digest'].split(':')[1]
with tarfile.open(fileobj=archive.extractfile(last),mode='r|*') as layer:
for member in layer:
name=member.name.lstrip('./');assert name in ('app','app/gitea','app/gitea/gitea')
if name=='app/gitea/gitea':
h=hashlib.sha256();stream=layer.extractfile(member)
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
assert h.hexdigest()==verified['binary_sha256'];found=True
assert found
save(work/'container-result.json',{'image_manifest_digest':descriptor['digest'],'config_digest':config_digest,
'oci_index_sha256':hashlib.sha256(index_bytes).hexdigest(),'archive_sha256':digest(archive_path),
'artifact_path':str(archive_path),'source_epoch':LOCK['source_date_epoch'],'container_epoch':epoch,
'all_base_layers_and_rootless_runtime_config_preserved':True,'all_oci_blob_digests_verified':True,
'final_layer_binary_exact':True,'registry_publication':False,'deployment_tag':None,
'host_or_secret_mounts':False,'privileged_containers':False,'local_runtime_image_loaded':False})
print((work/'container-result.json').read_text())
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container'])
parser.add_argument('--workspace',required=True)
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
a=parser.parse_args();work=Path(a.workspace).resolve()
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
if a.phase=='prepare':return prepare(a,work)
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
if a.phase=='acquire-npm':
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
{'build':build,'verify':verify,'container':container}[a.phase](work)
if __name__=='__main__':main()

View file

@ -0,0 +1,138 @@
# Actual Forgejo source-state transaction primitive, r1
This is bounded source progress for NK-WP-0048-T34 / RAPPS-WP-0016-T05/T08,
not an admitted operational backend or completed inverse. It adds real database
primitives to the actual reviewed Forgejo 11.0.3 source under `/tmp`; it adds no
second application source repository in Apps. No production/native database,
configuration, credential, deployment or identity was read or changed. Existing
frozen extension and release packets remain unchanged. No canonical owner edit,
publication, installed image selection or native dispatch occurred.
## Implemented database mechanics
`models/auth/source_operation.go` operates on actual `login_source` rows through
Forgejo's real transaction machinery. Prepare commits original whole source
inventory and exact private desired configuration before a source mutation.
Apply checks exact current/original equality, modifies one explicitly identified
OAuth2 source, verifies the complete expected persisted post-state and commits
post-state plus outcome atomically. Existing targets require exact ID/name/type;
case creation requires name absence. Configuration decoding is deliberately
avoided for snapshots and restoration, so unknown fields cannot silently vanish.
Complete raw private/public equality, source ID and created/updated identity
stay inside owner custody. Public results contain only operation ID, phase and
numeric source ID; no private digest or configuration is returned.
Inverse independently checks complete current/post equality. It restores
preexisting sources in place with the exact original raw bytes and ID. It never
deletes a preexisting or unchanged source. Only confirmed case creation can
select deletion; actual local user and external-login references refuse deletion
without changing those users/links. Another unresolved managed operation on the
same target refuses, rather than sharing creation provenance. Unknown or changed
state refuses. A prepared intent can be cancelled without changing sources.
Reconcile reads committed phase/current equality without retrying mutation.
Errors provide no driver/configuration text and return no successful result.
SQLite acquires a write transaction before source reads, excluding even
uncooperative SQL writers. The actual two-connection exclusion test sets the
managed engine to DEFERRED mode, so it exercises the explicit initial write.
The source contains PostgreSQL table locks covering source/journal/user/link
writers; PostgreSQL behavior is **not tested or accepted by this SQLite proof**.
MySQL and nested transactions refuse. Locks span each database transaction;
they do not span an entire release or control provider registration in another
process, DDL/admin bypasses, Helm/image writers or unmanaged shared consumers.
Complete application/release writer exclusion still needs owner implementation.
The production DB hooks currently print arguments on slow/error queries and
runtime traces, even with normal SQL logging disabled. This patch adds an
explicit private-query context and redacts statement, arguments and driver-error
text in those three hooks. The primitive refuses both configured SQL logging
and the actual XORM logger's argument logging. Server/driver/audit/backup logging,
custom hooks and private-runtime output custody remain owner review obligations.
The actual hook tests include positive unredacted controls; no real secrets
were used. The initial candidate only wrapped the Go context; inspection found
that existing Engined contexts and db.WithTx did not propagate that marker into
XORM's real query hooks. Direct hook fixtures alone missed this flaw. The final
patch rebinds marked contexts in db.GetEngine, preserving the same transaction
and actual SQL cancellation context; ordinary unmarked routing is unchanged.
The added actual hooked-XORM failing-query test checks real slow/error/trace
paths and positive/private controls. Earlier results remain in the evidence as
insufficient for this propagation question.
## Private journal custody and recovery limits
`login_source_operation` is registered with Forgejo's existing model/schema sync.
Its before/desired/post fields contain **private raw configurations and secret
copies**, including unrelated-source snapshots. It is not a value-free receipt,
independent authenticated evidence, an authorization token or an audit outbox.
It inherits the owner's database/storage/backup custody and encryption limits;
this patch adds no encryption or new read grant. Existing database access does
not automatically authorize an additional journal reader. Owner credential
inventory, recovery access, backup inclusion, retention and eventual deletion
must explicitly account for these additional copies before admission.
Preparation refuses at 64 unresolved or 256 total journals, at a desired config
above 64 KiB or an original inventory above 1 MiB. These are refusal bounds, not
retention approval. Nothing automatically erases unresolved, failed/uncertain or
terminal private recovery state. A separately authenticated owner retention and
purge decision/implementation is still required; exhaustion remains refused.
Completed rows preserve original/desired/post state for recovery. Individual
refused or storage-failed calls are not a separately durable attempt log: the
admitted runtime must preserve their original errors/uncertainty, current-session
references and authenticated attempt/evidence history outside a rolled-back
transaction. DB commit uncertainty always calls reconciliation; an ambiguous
response does not justify blindly replaying the effect.
The actual file-backed SQLite test kills a child process immediately after the
real ApplySourceOperation INSERT, before post-state/journal update/commit. A
new process sees the original prepared journal and no source. Later independent
processes observe confirmed apply, perform inverse and confirm reverted absence.
This proves bounded actual local database crash/restart mechanics, not an
external session, deployed database or independent evidence facility.
## Required integration before any exposed operation
There is intentionally no new unauthenticated CLI, API, privileged default
factory or always-true authority callback. These exported low-level functions
have the same private-database trust boundary as existing CRUD; they are not an
authorization boundary. An existing owner CLI/service must authenticate a fresh
current initiating principal/session and exact Access Engine decision **before
every call**, including preparation/reconciliation journal writes and each
inverse attempt. Admission must bind case, operation/transaction, source and
accepted implementation revision, exact parameters, target, principal, current
session, purpose/action and expiry/revocation. Neither an arbitrary Subject in
a CheckRequest, a service caller token, an offline config pin nor this journal
authenticates the initiating human/agent. Fresh inverse authority cannot reuse
a revoked hand-over session. All exit paths still need revocation.
The shared producer-owned Access Engine adapter is separate coordinator work.
The real owner session/evidence/outbox/revocation facilities, release-wide
exclusion, cross-process provider refresh/restart and session-to-CLI binding
remain unresolved here. These are existing hand-over/inverse obligations,
not an eighth SecurityCase kernel step or another service. Actual app policy
and exact desired-config validation remain the admitted owner's responsibility.
An image downgrade remains refused until authoritative source restoration/removal,
complete unrelated-state checks and all extension-sensitive configurations are
independently verified safe. Even a restored original source can contain a
preexisting extension gate; a source inverse does not itself authorize downgrade.
Helm omission/rollback never substitutes for source inverse. Keep the extended
image and gate when linked-source deletion refuses or any current state is
uncertain; preserve local sign-in and reconcile with the owner. Existing users
or external links must never be removed to force deletion.
## Evidence
The exact eight-path additive patch, actual source pre/postimages, inventories,
bounded real package tests, crash/exclusion tests and original failed attempts
are retained in this directory. Tests run in network-disabled bwrap with only
public source/toolchain/module inputs; toolchain/modules are read-only and build
cache/source are isolated writable copies. `run-tests.py` records the exact
sandbox and argv. Only synthetic fixtures are used. Initial Git-global-config
and hook-test pointer-method harness failures remain evidence, not successes.
Early test sandboxes used network isolation without a PID namespace; their
host-proc visibility is retained as a profile limitation, not an isolation proof.
No host process-argument scan was authored. Final and corrected control runs use
both network and PID isolation before mounting proc. The first control trial
started before its separate source copy completed; its missing-package setup
failure is explicitly invalid, preserved and superseded, never a detected guard.
No lifecycle, native gate or owner workplan is closed by these source tests.

View file

@ -0,0 +1,68 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"-run",
"TestSourceOperation",
"-count=1",
"-v"
],
"returncode": 1,
"seconds": 30.90784502029419
}

View file

@ -0,0 +1,68 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"-run",
"TestSourceOperation",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 16.431792497634888
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 1,
"seconds": 24.786890029907227
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 92.08943581581116
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 83.88910269737244
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 30.82476782798767
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 213.70628547668457
}

View file

@ -0,0 +1,70 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--unshare-pid",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--ro-bind",
"/tmp/forgejo-independent-r1/go",
"/tmp/forgejo-independent-r1/go",
"--ro-bind",
"/tmp/forgejo-independent-r1/gomodcache",
"/tmp/forgejo-independent-r1/gomodcache",
"--bind",
"/tmp/apps-operation-backend-r1",
"/tmp/apps-operation-backend-r1",
"--bind",
"/tmp/apps-operation-gocache-r1",
"/tmp/apps-operation-gocache-r1",
"--chdir",
"/tmp/apps-operation-backend-r1",
"--",
"env",
"-i",
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
"HOME=/tmp/operation-home",
"GOTOOLCHAIN=local",
"GOPROXY=off",
"GOSUMDB=off",
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
"GOCACHE=/tmp/apps-operation-gocache-r1",
"GOPATH=/tmp/operation-gopath",
"GOVCS=*:off",
"GIT_CONFIG_NOSYSTEM=1",
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
"GIT_TERMINAL_PROMPT=0",
"GOMAXPROCS=4",
"CGO_ENABLED=1",
"go",
"test",
"-p",
"4",
"-tags",
"sqlite sqlite_unlock_notify",
"./models/auth",
"./models/db",
"-run",
"TestSourceOperation|TestSensitiveSQLHooks",
"-count=1",
"-v"
],
"returncode": 0,
"seconds": 46.296929121017456
}

View file

@ -0,0 +1,188 @@
--- a/models/db/engine.go
+++ b/models/db/engine.go
@@ -330,8 +330,12 @@
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, task := trace.NewTask(c.Ctx, "sql")
ctx = context.WithValue(ctx, sqlTask{}, task)
- trace.Log(ctx, "query", c.SQL)
- trace.Logf(ctx, "args", "%v", c.Args)
+ if isSensitiveSQL(c.Ctx) {
+ trace.Log(ctx, "query", "[owner-confined query]")
+ } else {
+ trace.Log(ctx, "query", c.SQL)
+ trace.Logf(ctx, "args", "%v", c.Args)
+ }
return ctx, nil
}
@@ -353,7 +357,11 @@
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.ExecuteTime >= h.Treshold {
- h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
+ if isSensitiveSQL(c.Ctx) {
+ h.Logger.Log(8, log.WARN, "[Slow SQL Query] [owner-confined query] - %v", c.ExecuteTime)
+ } else {
+ h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
+ }
}
return nil
}
@@ -370,7 +378,11 @@
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
- h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
- }
- return nil
-}
+ if isSensitiveSQL(c.Ctx) {
+ h.Logger.Log(8, log.ERROR, "[Error SQL Query] [owner-confined query]")
+ } else {
+ h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
+ }
+ }
+ return nil
+}
--- a/models/db/context.go
+++ b/models/db/context.go
@@ -72,6 +72,12 @@
// GetEngine will get a db Engine from this context or return an Engine restricted to this context
func GetEngine(ctx context.Context) Engine {
if e := getEngine(ctx); e != nil {
+ // A private-query marker must reach XORM's real query hooks, including
+ // inside an existing engine/transaction context. Wrapping the Go
+ // context alone does not change XORM's stored session context.
+ if isSensitiveSQL(ctx) {
+ return e.Context(ctx)
+ }
return e
}
return x.Context(ctx)
--- /dev/null
+++ b/models/db/sensitive_sql.go
@@ -0,0 +1,18 @@
+// Copyright 2026 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+package db
+
+import "context"
+
+type sensitiveSQLKey struct{}
+
+// WithSensitiveSQL suppresses argument, statement and driver-error disclosure
+// in the application's query hooks. The caller must separately refuse raw XORM
+// SQL logging; custom driver/server auditing remains an owner custody concern.
+func WithSensitiveSQL(ctx context.Context) context.Context {
+ return context.WithValue(ctx, sensitiveSQLKey{}, true)
+}
+func isSensitiveSQL(ctx context.Context) bool {
+ sensitive, _ := ctx.Value(sensitiveSQLKey{}).(bool)
+ return sensitive
+}
--- /dev/null
+++ b/models/db/sensitive_sql_test.go
@@ -0,0 +1,103 @@
+// Copyright 2026 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+package db
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "runtime/trace"
+ "strings"
+ "testing"
+ "time"
+
+ "forgejo.org/modules/log"
+ "xorm.io/xorm"
+ "xorm.io/xorm/contexts"
+)
+
+type sensitiveSQLLog struct {
+ log.Logger
+ entries []string
+}
+
+func (l *sensitiveSQLLog) Log(_ int, _ log.Level, f string, v ...any) {
+ l.entries = append(l.entries, fmt.Sprintf(f, v...))
+}
+func TestSensitiveSQLHooks(t *testing.T) {
+ const secret = "synthetic-query-private-sentinel"
+ for _, sensitive := range []bool{false, true} {
+ ctx := context.Background()
+ if sensitive {
+ ctx = WithSensitiveSQL(ctx)
+ }
+ h := &contexts.ContextHook{Ctx: ctx, SQL: "SELECT '" + secret + "'", Args: []any{secret}, Err: errors.New(secret), ExecuteTime: time.Second}
+ logger := new(sensitiveSQLLog)
+ if err := (&SlowQueryHook{Logger: logger}).AfterProcess(h); err != nil {
+ t.Fatal(err)
+ }
+ if err := (&ErrorQueryHook{Logger: logger}).AfterProcess(h); err != nil {
+ t.Fatal(err)
+ }
+ found := strings.Contains(strings.Join(logger.entries, "\n"), secret)
+ if found == sensitive {
+ t.Fatal("query hook disclosure/redaction mismatch")
+ }
+ var out bytes.Buffer
+ if err := trace.Start(&out); err != nil {
+ t.Fatal(err)
+ }
+ next, err := (TracingHook{}).BeforeProcess(h)
+ if err != nil {
+ trace.Stop()
+ t.Fatal(err)
+ }
+ h.Ctx = next
+ err = (TracingHook{}).AfterProcess(h)
+ trace.Stop()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
+ t.Fatal("trace disclosure/redaction mismatch")
+ }
+ }
+}
+
+func TestSensitiveSQLActualContextPropagation(t *testing.T) {
+ engine, ok := GetEngine(DefaultContext).(*xorm.Engine)
+ if !ok {
+ t.Fatal("missing actual fixture engine")
+ }
+ logger := new(sensitiveSQLLog)
+ engine.AddHook(&SlowQueryHook{Logger: logger})
+ engine.AddHook(&ErrorQueryHook{Logger: logger})
+ engine.AddHook(TracingHook{})
+ const secret = "synthetic-actual-xorm-private-sentinel"
+ for _, sensitive := range []bool{false, true} {
+ logger.entries = nil
+ ctx := context.Context(DefaultContext)
+ if sensitive {
+ ctx = WithSensitiveSQL(ctx)
+ }
+ var out bytes.Buffer
+ if err := trace.Start(&out); err != nil {
+ t.Fatal(err)
+ }
+ _, err := GetEngine(ctx).Exec("SELECT missing_synthetic_function(?)", secret)
+ trace.Stop()
+ if err == nil {
+ t.Fatal("expected actual SQLite function refusal")
+ }
+ if len(logger.entries) < 2 {
+ t.Fatal("actual query hooks did not execute")
+ }
+ if strings.Contains(strings.Join(logger.entries, "\n"), secret) == sensitive {
+ t.Fatal("actual XORM query context did not preserve redaction")
+ }
+ if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
+ t.Fatal("actual XORM trace context did not preserve redaction")
+ }
+ }
+}

View file

@ -0,0 +1,430 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
// This owner-confined database primitive is NOT an authorization boundary.
// No CLI/web/API route exposes it. The admitted runtime must separately obtain
// fresh authenticated apply/inverse sessions and retain signed public evidence.
// Configurations and private journal rows must never leave database custody.
import (
"context"
"encoding/json"
"errors"
"reflect"
"regexp"
"sort"
"forgejo.org/models/db"
"forgejo.org/modules/setting"
"forgejo.org/modules/timeutil"
"xorm.io/xorm"
)
var (
ErrSourceOperationConflict = errors.New("source operation requires owner reconciliation")
ErrSourceOperationStorage = errors.New("source operation storage failure; reconcile before retry")
ErrSourceOperationUnsupported = errors.New("source operation requires standalone SQLite/PostgreSQL transaction with SQL logging disabled")
operationIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{7,127}$`)
)
// Private source rows deliberately bypass Config decoding: decoding and
// reserializing can erase unknown fields, including a gate on an older image.
type sourceOperationRow struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string
IsActive bool
IsSyncEnabled bool
Cfg string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp
UpdatedUnix timeutil.TimeStamp
}
func (sourceOperationRow) TableName() string { return "login_source" }
// The whole inventory, including private configs, stays in the owner's database.
// Neither journal payload nor its digest is a public result.
type sourceOperationJournal struct {
OperationID string `xorm:"pk VARCHAR(128)"`
CaseID string `xorm:"VARCHAR(128)"`
TargetName string
TargetID int64
Phase string
BeforeState string `xorm:"TEXT"`
DesiredState string `xorm:"TEXT"`
PostState string `xorm:"TEXT"`
}
func (sourceOperationJournal) TableName() string { return "login_source_operation" }
func init() { db.RegisterModel(new(sourceOperationJournal)) }
// SourceOperationResult contains only public outcome metadata. An authenticated
// session/evidence producer must bind this result to its actual principal,
// admitted source revision, exact parameters and current operation session.
type SourceOperationResult struct {
OperationID string
Phase string
SourceID int64
}
// sourceOperationTx acquires a database-enforced lock before any source read.
// SQLite's first write serializes every database writer, including stock admin
// CRUD. PostgreSQL table locks exclude ordinary SQL source and user-link writes.
// Privileged DDL, process/provider refresh and release writers are outside this
// bounded primitive; they still require owner runtime/release exclusion.
func sourceOperationTx(ctx context.Context, f func(context.Context) error) error {
if db.InTransaction(ctx) || setting.Database.LogSQL || (!setting.Database.Type.IsSQLite3() && !setting.Database.Type.IsPostgreSQL()) {
return ErrSourceOperationUnsupported
}
err := db.WithTx(db.WithSensitiveSQL(ctx), func(tx context.Context) error {
session, ok := db.GetEngine(tx).(*xorm.Session)
if !ok || session.Engine().Logger().IsShowSQL() {
return ErrSourceOperationUnsupported
}
if setting.Database.Type.IsPostgreSQL() {
if _, err := db.GetEngine(tx).Exec(`LOCK TABLE login_source_operation, login_source, "user", external_login_user IN EXCLUSIVE MODE`); err != nil {
return err
}
} else {
// Even with no matching row, UPDATE starts SQLite's write transaction.
if _, err := db.GetEngine(tx).Exec(`UPDATE login_source_operation SET phase = phase WHERE operation_id = ''`); err != nil {
return err
}
}
return f(tx)
})
if err == nil || errors.Is(err, ErrSourceOperationConflict) || errors.Is(err, ErrSourceOperationUnsupported) || IsErrSourceInUse(err) {
return err
}
return ErrSourceOperationStorage
}
func sourceInventory(ctx context.Context) ([]sourceOperationRow, error) {
rows := make([]sourceOperationRow, 0)
err := db.GetEngine(ctx).OrderBy("id").Find(&rows)
return rows, err
}
func encodeSourceInventory(rows []sourceOperationRow) string {
b, _ := json.Marshal(rows)
return string(b)
}
func decodeSourceInventory(s string) ([]sourceOperationRow, error) {
var rows []sourceOperationRow
if err := json.Unmarshal([]byte(s), &rows); err != nil {
return nil, ErrSourceOperationConflict
}
return rows, nil
}
func sourceJournal(ctx context.Context, operationID string) (*sourceOperationJournal, error) {
row := new(sourceOperationJournal)
found, err := db.GetEngine(ctx).ID(operationID).Get(row)
if err != nil {
return nil, err
}
if !found {
return nil, ErrSourceOperationConflict
}
return row, nil
}
func journalResult(j *sourceOperationJournal) SourceOperationResult {
return SourceOperationResult{OperationID: j.OperationID, Phase: j.Phase, SourceID: j.TargetID}
}
// PrepareSourceOperation durably captures exact source identity and complete
// original inventory before mutation. desired is private owner input, not an
// authorization token. targetID=0 requires authoritative name absence. Existing
// sources require both exact ID and name. Reuse of an operation ID is refused.
func PrepareSourceOperation(ctx context.Context, operationID, caseID string, targetID int64, desired *Source) (SourceOperationResult, error) {
var result SourceOperationResult
if !operationIDPattern.MatchString(operationID) || !operationIDPattern.MatchString(caseID) || desired == nil || desired.Type != OAuth2 || desired.Name == "" || desired.Cfg == nil || desired.IsSyncEnabled || desired.ID != targetID {
return result, ErrSourceOperationConflict
}
cfg, err := desired.Cfg.ToDB()
if err != nil || len(cfg) > 64*1024 || !json.Valid(cfg) {
return result, ErrSourceOperationConflict
}
candidate := sourceOperationRow{ID: targetID, Type: OAuth2, Name: desired.Name, IsActive: desired.IsActive, Cfg: string(cfg)}
candidateBytes, _ := json.Marshal(candidate)
err = sourceOperationTx(ctx, func(tx context.Context) error {
// Bound confidential duplicate custody. Refuse rather than erase journals
// with pending recovery or silently expire terminal private snapshots.
count, err := db.GetEngine(tx).Count(new(sourceOperationJournal))
if err != nil {
return err
}
if count >= 256 {
return ErrSourceOperationConflict
}
active, err := db.GetEngine(tx).Where("phase IN (?, ?, ?)", "prepared", "applied", "unchanged").Count(new(sourceOperationJournal))
if err != nil {
return err
}
if active >= 64 {
return ErrSourceOperationConflict
}
found, err := db.GetEngine(tx).ID(operationID).Exist(new(sourceOperationJournal))
if err != nil {
return err
}
if found {
return ErrSourceOperationConflict
}
shared, err := db.GetEngine(tx).Where("target_name = ? AND phase IN (?, ?, ?)", desired.Name, "prepared", "applied", "unchanged").Exist(new(sourceOperationJournal))
if err != nil {
return err
}
if shared {
return ErrSourceOperationConflict
}
rows, err := sourceInventory(tx)
if err != nil {
return err
}
if len(encodeSourceInventory(rows)) > 1024*1024 {
return ErrSourceOperationConflict
}
matched := false
for _, row := range rows {
if row.Name == desired.Name || row.ID == targetID {
if targetID == 0 || row.ID != targetID || row.Name != desired.Name || row.Type != OAuth2 {
return ErrSourceOperationConflict
}
matched = true
}
}
if targetID != 0 && !matched {
return ErrSourceOperationConflict
}
j := &sourceOperationJournal{OperationID: operationID, CaseID: caseID, TargetName: desired.Name, TargetID: targetID, Phase: "prepared", BeforeState: encodeSourceInventory(rows), DesiredState: string(candidateBytes)}
if _, err := db.GetEngine(tx).Insert(j); err != nil {
return err
}
result = journalResult(j)
return nil
})
if err != nil {
return SourceOperationResult{}, err
}
return result, nil
}
// ApplySourceOperation atomically compares every original raw source row,
// mutates exactly the target and persists confirmed post-state. A lost commit
// acknowledgement is reconciled using the same operation ID, never a fresh ID.
// No source registration/network call occurs inside the database transaction.
func ApplySourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
var result SourceOperationResult
err := sourceOperationTx(ctx, func(tx context.Context) error {
j, err := sourceJournal(tx, operationID)
if err != nil {
return err
}
if j.CaseID != caseID {
return ErrSourceOperationConflict
}
rows, err := sourceInventory(tx)
if err != nil {
return err
}
if j.Phase == "applied" || j.Phase == "unchanged" {
if encodeSourceInventory(rows) != j.PostState {
return ErrSourceOperationConflict
}
result = journalResult(j)
return nil
}
if j.Phase != "prepared" || encodeSourceInventory(rows) != j.BeforeState {
return ErrSourceOperationConflict
}
var desired sourceOperationRow
if json.Unmarshal([]byte(j.DesiredState), &desired) != nil {
return ErrSourceOperationConflict
}
unchanged := false
for _, row := range rows {
if row.ID == j.TargetID {
desired.CreatedUnix = row.CreatedUnix
desired.UpdatedUnix = row.UpdatedUnix
unchanged = reflect.DeepEqual(row, desired)
}
}
if !unchanged {
desired.UpdatedUnix = timeutil.TimeStampNow()
if j.TargetID == 0 {
desired.CreatedUnix = desired.UpdatedUnix
if _, err := db.GetEngine(tx).NoAutoTime().Insert(&desired); err != nil {
return err
}
j.TargetID = desired.ID
} else {
n, err := db.GetEngine(tx).ID(j.TargetID).NoAutoTime().AllCols().Update(&desired)
if err != nil {
return err
}
if n != 1 {
return ErrSourceOperationConflict
}
}
}
post, err := sourceInventory(tx)
if err != nil {
return err
}
expected := append([]sourceOperationRow{}, rows...)
if !unchanged {
replaced := false
for i := range expected {
if expected[i].ID == desired.ID {
expected[i] = desired
replaced = true
}
}
if !replaced {
expected = append(expected, desired)
}
sort.Slice(expected, func(i, k int) bool { return expected[i].ID < expected[k].ID })
}
if !reflect.DeepEqual(post, expected) {
return ErrSourceOperationConflict
}
j.PostState = encodeSourceInventory(post)
j.Phase = "applied"
if unchanged {
j.Phase = "unchanged"
}
if _, err := db.GetEngine(tx).ID(j.OperationID).AllCols().Update(j); err != nil {
return err
}
result = journalResult(j)
return nil
})
if err != nil {
return SourceOperationResult{}, err
}
return result, nil
}
// InverseSourceOperation requires fresh external inverse authority on EACH call.
// It restores a preexisting source in place; only confirmed case creation can
// delete. Any linked local/external account refuses deletion without alteration.
func InverseSourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
var result SourceOperationResult
err := sourceOperationTx(ctx, func(tx context.Context) error {
j, err := sourceJournal(tx, operationID)
if err != nil {
return err
}
if j.CaseID != caseID {
return ErrSourceOperationConflict
}
rows, err := sourceInventory(tx)
if err != nil {
return err
}
if j.Phase == "reverted" || j.Phase == "cancelled" {
if encodeSourceInventory(rows) != j.BeforeState {
return ErrSourceOperationConflict
}
result = journalResult(j)
return nil
}
if j.Phase == "prepared" {
if encodeSourceInventory(rows) != j.BeforeState {
return ErrSourceOperationConflict
}
j.Phase = "cancelled"
} else {
if (j.Phase != "applied" && j.Phase != "unchanged") || encodeSourceInventory(rows) != j.PostState {
return ErrSourceOperationConflict
}
before, err := decodeSourceInventory(j.BeforeState)
if err != nil {
return err
}
var original *sourceOperationRow
for i := range before {
if before[i].ID == j.TargetID {
original = &before[i]
}
}
if original != nil {
if _, err := db.GetEngine(tx).ID(j.TargetID).NoAutoTime().AllCols().Update(original); err != nil {
return err
}
} else {
for _, table := range []struct{ name, column string }{{"user", "login_source"}, {"external_login_user", "login_source_id"}} {
count, err := db.GetEngine(tx).Table(table.name).Where(table.column+" = ?", j.TargetID).Count()
if err != nil {
return err
}
if count != 0 {
return ErrSourceInUse{ID: j.TargetID}
}
}
n, err := db.GetEngine(tx).ID(j.TargetID).Delete(new(sourceOperationRow))
if err != nil {
return err
}
if n != 1 {
return ErrSourceOperationConflict
}
}
restored, err := sourceInventory(tx)
if err != nil {
return err
}
if encodeSourceInventory(restored) != j.BeforeState {
return ErrSourceOperationConflict
}
j.Phase = "reverted"
}
if _, err := db.GetEngine(tx).ID(j.OperationID).AllCols().Update(j); err != nil {
return err
}
result = journalResult(j)
return nil
})
if err != nil {
return SourceOperationResult{}, err
}
return result, nil
}
// ReconcileSourceOperation only checks committed journal/current equality. It
// never infers that a prepared intent ran and never retries the mutation. The
// separately authenticated runtime must call this after an ambiguous result.
func ReconcileSourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
var result SourceOperationResult
err := sourceOperationTx(ctx, func(tx context.Context) error {
j, err := sourceJournal(tx, operationID)
if err != nil {
return err
}
if j.CaseID != caseID {
return ErrSourceOperationConflict
}
rows, err := sourceInventory(tx)
if err != nil {
return err
}
expected := j.BeforeState
switch j.Phase {
case "prepared", "reverted", "cancelled":
case "applied", "unchanged":
expected = j.PostState
default:
return ErrSourceOperationConflict
}
if encodeSourceInventory(rows) != expected {
return ErrSourceOperationConflict
}
result = journalResult(j)
return nil
})
if err != nil {
return SourceOperationResult{}, err
}
return result, nil
}

View file

@ -0,0 +1,87 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"testing"
"time"
"forgejo.org/models/db"
"xorm.io/xorm"
)
func TestSourceOperationDatabaseWriterExclusion(t *testing.T) {
// Use DEFERRED transaction mode in the managed engine as well, so this
// test detects removal of the primitive's explicit first write.
previousContext := db.DefaultContext
previousEngine, ok := db.GetEngine(previousContext).(*xorm.Engine)
if !ok {
t.Fatal("missing actual fixture engine")
}
managed, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=deferred")
if err != nil {
t.Fatal(err)
}
db.SetDefaultEngine(context.Background(), managed)
defer func() { db.SetDefaultEngine(previousContext, previousEngine); managed.Close() }()
// A second actual connection executes uncooperative raw SQL, so this checks
// database exclusion rather than an advisory-lock convention.
raw, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=deferred")
if err != nil {
t.Fatal(err)
}
defer raw.Close()
locked := make(chan struct{})
release := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- sourceOperationTx(db.DefaultContext, func(ctx context.Context) error { close(locked); <-release; return nil })
}()
select {
case <-locked:
case err := <-done:
t.Fatalf("lock failed: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("lock timed out")
}
writeDone := make(chan error, 1)
go func() { _, err := raw.Exec("UPDATE login_source SET name = name WHERE id = -123"); writeDone <- err }()
select {
case err := <-writeDone:
close(release)
<-done
t.Fatalf("uncooperative write escaped exclusion: %v", err)
case <-time.After(100 * time.Millisecond):
}
close(release)
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("transaction did not finish")
}
select {
case err := <-writeDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("writer did not resume")
}
}
func TestSourceOperationRefusesActualXORMArgumentLogging(t *testing.T) {
engine, ok := db.GetEngine(db.DefaultContext).(*xorm.Engine)
if !ok {
t.Fatal("missing actual engine")
}
engine.ShowSQL(true)
defer engine.ShowSQL(false)
called := false
err := sourceOperationTx(db.DefaultContext, func(context.Context) error { called = true; return nil })
if err != ErrSourceOperationUnsupported || called {
t.Fatal("actual SQL argument logger did not fail closed")
}
}

View file

@ -0,0 +1,97 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth_test
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
auth "forgejo.org/models/auth"
"forgejo.org/models/db"
oauth2 "forgejo.org/services/auth/source/oauth2"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
"xorm.io/xorm/contexts"
"xorm.io/xorm/names"
)
type operationCrashAfterSourceInsert struct{}
func (operationCrashAfterSourceInsert) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (operationCrashAfterSourceInsert) AfterProcess(c *contexts.ContextHook) error {
if c.Err == nil && strings.Contains(c.SQL, "INSERT INTO") && strings.Contains(c.SQL, "login_source") && !strings.Contains(c.SQL, "login_source_operation") {
// Process death after the real INSERT, before actual ApplySourceOperation's
// post-read/journal update/commit. No artificial operation is substituted.
os.Exit(73)
}
return nil
}
func TestSourceOperationProcessRecovery(t *testing.T) {
if step := os.Getenv("SOURCE_OPERATION_CHILD_STEP"); step != "" {
e, err := xorm.NewEngine("sqlite3", os.Getenv("SOURCE_OPERATION_CHILD_DB")+"?_txlock=deferred")
require.NoError(t, err)
e.SetMapper(names.GonicMapper{})
db.SetDefaultEngine(context.Background(), e)
require.NoError(t, db.SyncAllTables())
auth.RegisterTypeConfig(auth.OAuth2, new(oauth2.Source))
ctx := db.DefaultContext
switch step {
case "prepare":
_, err = auth.PrepareSourceOperation(ctx, "operation-process-recovery", "case-process-recovery", 0, operationSource())
require.NoError(t, err)
case "crash":
e.AddHook(operationCrashAfterSourceInsert{})
_, err = auth.ApplySourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
t.Fatal("real insert did not reach crash hook")
case "recover-prepared":
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
require.Equal(t, "prepared", r.Phase)
n, err := db.GetEngine(ctx).Table("login_source").Count()
require.NoError(t, err)
require.Zero(t, n)
_, err = auth.ApplySourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
case "recover-applied":
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
require.Equal(t, "applied", r.Phase)
_, err = auth.InverseSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
case "recover-inverse":
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
require.NoError(t, err)
require.Equal(t, "reverted", r.Phase)
n, err := db.GetEngine(ctx).Table("login_source").Count()
require.NoError(t, err)
require.Zero(t, n)
default:
t.Fatal("unknown synthetic child step")
}
require.NoError(t, e.Close())
return
}
exe, err := os.Executable()
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "owner-private.sqlite")
for _, step := range []string{"prepare", "crash", "recover-prepared", "recover-applied", "recover-inverse"} {
child := exec.Command(exe, "-test.run=^TestSourceOperationProcessRecovery$", "-test.v")
child.Env = append(os.Environ(), "SOURCE_OPERATION_CHILD_STEP="+step, "SOURCE_OPERATION_CHILD_DB="+path)
output, err := child.CombinedOutput()
if step == "crash" {
exit, ok := err.(*exec.ExitError)
require.True(t, ok, string(output))
require.Equal(t, 73, exit.ExitCode(), string(output))
} else {
require.NoError(t, err, string(output))
}
}
}

View file

@ -0,0 +1,235 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth_test
import (
"context"
"encoding/json"
"fmt"
"testing"
auth "forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/models/unittest"
"forgejo.org/modules/setting"
oauth2 "forgejo.org/services/auth/source/oauth2"
"github.com/stretchr/testify/require"
)
func operationDB(t *testing.T) context.Context {
t.Helper()
require.NoError(t, unittest.PrepareTestDatabase())
_, err := db.GetEngine(db.DefaultContext).Exec("DELETE FROM login_source_operation")
require.NoError(t, err)
auth.RegisterTypeConfig(auth.OAuth2, new(oauth2.Source))
return db.DefaultContext
}
func operationSource() *auth.Source {
return &auth.Source{Type: auth.OAuth2, Name: "operation-test", IsActive: false, Cfg: &oauth2.Source{Provider: "openidConnect", ClientID: "synthetic-client", ClientSecret: "synthetic-private-original", RequiredClaimName: "groups", RequiredClaimValues: []string{"g1", "g2", "g3"}, Scopes: []string{"openid", "profile", "groups"}}}
}
func operationRows(t *testing.T, ctx context.Context) string {
t.Helper()
rows, err := db.GetEngine(ctx).Query("SELECT * FROM login_source ORDER BY id")
require.NoError(t, err)
b, err := json.Marshal(rows)
require.NoError(t, err)
return string(b)
}
func TestSourceOperationCreateAndInverse(t *testing.T) {
ctx := operationDB(t)
before := operationRows(t, ctx)
r, err := auth.PrepareSourceOperation(ctx, "operation-create", "case-create", 0, operationSource())
require.NoError(t, err)
require.Equal(t, "prepared", r.Phase)
require.Equal(t, before, operationRows(t, ctx))
r, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
require.NoError(t, err)
require.Equal(t, "applied", r.Phase)
require.Positive(t, r.SourceID)
id := r.SourceID
post := operationRows(t, ctx)
r, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
require.NoError(t, err)
require.Equal(t, id, r.SourceID)
require.Equal(t, post, operationRows(t, ctx))
_, err = auth.InverseSourceOperation(ctx, "operation-create", "wrong-case")
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
r, err = auth.InverseSourceOperation(ctx, "operation-create", "case-create")
require.NoError(t, err)
require.Equal(t, "reverted", r.Phase)
require.Equal(t, before, operationRows(t, ctx))
_, err = auth.InverseSourceOperation(ctx, "operation-create", "case-create")
require.NoError(t, err)
_, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
}
func TestSourceOperationRestoresExactRawPreexisting(t *testing.T) {
ctx := operationDB(t)
s := operationSource()
require.NoError(t, auth.CreateSource(ctx, s))
// The old/unknown field must survive restoration byte for byte, including the
// private synthetic secret. A typed OAuth2 decoder would silently lose it.
raw := `{"Provider":"openidConnect","ClientID":"synthetic-client","ClientSecret":"synthetic-private-original","FutureGuard":{"x":true}}`
_, err := db.GetEngine(ctx).Exec("UPDATE login_source SET cfg = ? WHERE id = ?", raw, s.ID)
require.NoError(t, err)
before := operationRows(t, ctx)
s.Cfg.(*oauth2.Source).ClientSecret = "synthetic-private-new"
_, err = auth.PrepareSourceOperation(ctx, "operation-restore", "case-restore", s.ID, s)
require.NoError(t, err)
_, err = auth.ApplySourceOperation(ctx, "operation-restore", "case-restore")
require.NoError(t, err)
_, err = auth.InverseSourceOperation(ctx, "operation-restore", "case-restore")
require.NoError(t, err)
require.Equal(t, before, operationRows(t, ctx))
source, err := auth.GetSourceByID(ctx, s.ID)
require.NoError(t, err)
require.Equal(t, s.ID, source.ID)
}
func TestSourceOperationUnchangedNeverDeletesPreexisting(t *testing.T) {
ctx := operationDB(t)
s := operationSource()
require.NoError(t, auth.CreateSource(ctx, s))
before := operationRows(t, ctx)
_, err := auth.PrepareSourceOperation(ctx, "operation-unchanged", "case-unchanged", s.ID, s)
require.NoError(t, err)
r, err := auth.ApplySourceOperation(ctx, "operation-unchanged", "case-unchanged")
require.NoError(t, err)
require.Equal(t, "unchanged", r.Phase)
_, err = auth.InverseSourceOperation(ctx, "operation-unchanged", "case-unchanged")
require.NoError(t, err)
require.Equal(t, before, operationRows(t, ctx))
}
func TestSourceOperationPrivateDriftRefuses(t *testing.T) {
for _, stage := range []string{"before-apply", "before-inverse"} {
t.Run(stage, func(t *testing.T) {
ctx := operationDB(t)
s := operationSource()
require.NoError(t, auth.CreateSource(ctx, s))
_, err := auth.PrepareSourceOperation(ctx, "operation-private-drift", "case-private-drift", s.ID, s)
require.NoError(t, err)
if stage == "before-inverse" {
_, err = auth.ApplySourceOperation(ctx, "operation-private-drift", "case-private-drift")
require.NoError(t, err)
}
s.Cfg.(*oauth2.Source).ClientSecret = "synthetic-private-drift"
require.NoError(t, auth.UpdateSource(ctx, s))
drift := operationRows(t, ctx)
if stage == "before-apply" {
_, err = auth.ApplySourceOperation(ctx, "operation-private-drift", "case-private-drift")
} else {
_, err = auth.InverseSourceOperation(ctx, "operation-private-drift", "case-private-drift")
}
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
require.NotContains(t, err.Error(), "synthetic-private")
require.Equal(t, drift, operationRows(t, ctx))
})
}
}
func TestSourceOperationStockCRUDDriftRefuses(t *testing.T) {
ctx := operationDB(t)
_, err := auth.PrepareSourceOperation(ctx, "operation-other-writer", "case-other-writer", 0, operationSource())
require.NoError(t, err)
other := operationSource()
other.Name = "unrelated-source"
require.NoError(t, auth.CreateSource(ctx, other))
drift := operationRows(t, ctx)
_, err = auth.ApplySourceOperation(ctx, "operation-other-writer", "case-other-writer")
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
require.Equal(t, drift, operationRows(t, ctx))
}
func TestSourceOperationLinkedSourceRefusesDeletion(t *testing.T) {
for _, kind := range []string{"user", "external"} {
t.Run(kind, func(t *testing.T) {
ctx := operationDB(t)
_, err := auth.PrepareSourceOperation(ctx, "operation-linked", "case-linked", 0, operationSource())
require.NoError(t, err)
r, err := auth.ApplySourceOperation(ctx, "operation-linked", "case-linked")
require.NoError(t, err)
if kind == "user" {
_, err = db.GetEngine(ctx).Exec("UPDATE `user` SET login_source = ? WHERE id = 1", r.SourceID)
} else {
_, err = db.GetEngine(ctx).Exec("INSERT INTO external_login_user (external_id, user_id, login_source_id) VALUES (?, ?, ?)", "synthetic-external", 1, r.SourceID)
}
require.NoError(t, err)
before := operationRows(t, ctx)
_, err = auth.InverseSourceOperation(ctx, "operation-linked", "case-linked")
require.True(t, auth.IsErrSourceInUse(err))
require.Equal(t, before, operationRows(t, ctx))
if kind == "user" {
rows, e := db.GetEngine(ctx).Query("SELECT login_source FROM `user` WHERE id = 1")
require.NoError(t, e)
require.NotEqual(t, []byte("0"), rows[0]["login_source"])
} else {
n, e := db.GetEngine(ctx).Table("external_login_user").Count()
require.NoError(t, e)
require.EqualValues(t, 1, n)
}
})
}
}
func TestSourceOperationPreparedCancellationAndIDReuse(t *testing.T) {
ctx := operationDB(t)
before := operationRows(t, ctx)
_, err := auth.PrepareSourceOperation(ctx, "operation-cancelled", "case-cancelled", 0, operationSource())
require.NoError(t, err)
_, err = auth.PrepareSourceOperation(ctx, "operation-cancelled", "case-cancelled", 0, operationSource())
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
r, err := auth.InverseSourceOperation(ctx, "operation-cancelled", "case-cancelled")
require.NoError(t, err)
require.Equal(t, "cancelled", r.Phase)
require.Equal(t, before, operationRows(t, ctx))
}
func TestSourceOperationRejectsUnsafeRuntime(t *testing.T) {
ctx := operationDB(t)
setting.Database.LogSQL = true
t.Cleanup(func() { setting.Database.LogSQL = false })
_, err := auth.PrepareSourceOperation(ctx, "operation-no-logging", "case-no-logging", 0, operationSource())
require.ErrorIs(t, err, auth.ErrSourceOperationUnsupported)
setting.Database.LogSQL = false
err = db.WithTx(ctx, func(tx context.Context) error {
_, err := auth.PrepareSourceOperation(tx, "operation-nested", "case-nested", 0, operationSource())
return err
})
require.ErrorIs(t, err, auth.ErrSourceOperationUnsupported)
}
func TestSourceOperationRefusesSharedManagedSource(t *testing.T) {
ctx := operationDB(t)
_, err := auth.PrepareSourceOperation(ctx, "operation-first-case", "case-first-case", 0, operationSource())
require.NoError(t, err)
r, err := auth.ApplySourceOperation(ctx, "operation-first-case", "case-first-case")
require.NoError(t, err)
shared := operationSource()
shared.ID = r.SourceID
_, err = auth.PrepareSourceOperation(ctx, "operation-second-case", "case-second-case", r.SourceID, shared)
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
}
func TestSourceOperationBoundsPrivateJournalCustody(t *testing.T) {
ctx := operationDB(t)
// A full unresolved-journal budget refuses new preparation, without deleting
// any intent or replacing private recovery data with an expiry assumption.
for i := 0; i < 64; i++ {
_, err := db.GetEngine(ctx).Exec("INSERT INTO login_source_operation (operation_id, case_id, target_name, phase) VALUES (?, ?, ?, ?)", fmt.Sprintf("bounded-operation-%03d", i), "bounded-case", fmt.Sprintf("bounded-target-%03d", i), "prepared")
require.NoError(t, err)
}
_, err := auth.PrepareSourceOperation(ctx, "operation-budget-full", "case-budget-full", 0, operationSource())
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
n, err := db.GetEngine(ctx).Table("login_source_operation").Count()
require.NoError(t, err)
require.EqualValues(t, 64, n)
}
func TestSourceOperationRequiresExactPostState(t *testing.T) {
ctx := operationDB(t)
before := operationRows(t, ctx)
_, err := auth.PrepareSourceOperation(ctx, "operation-post-exact", "case-post-exact", 0, operationSource())
require.NoError(t, err)
_, err = db.GetEngine(ctx).Exec("CREATE TRIGGER source_operation_test_drift AFTER INSERT ON login_source BEGIN UPDATE login_source SET name = 'unexpected-rewrite' WHERE id = NEW.id; END")
require.NoError(t, err)
defer db.GetEngine(ctx).Exec("DROP TRIGGER source_operation_test_drift")
_, err = auth.ApplySourceOperation(ctx, "operation-post-exact", "case-post-exact")
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
require.Equal(t, before, operationRows(t, ctx))
r, err := auth.ReconcileSourceOperation(ctx, "operation-post-exact", "case-post-exact")
require.NoError(t, err)
require.Equal(t, "prepared", r.Phase)
}

View file

@ -0,0 +1,388 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"reflect"
"runtime/trace"
"strings"
"time"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"xorm.io/xorm"
"xorm.io/xorm/contexts"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas"
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
_ "github.com/lib/pq" // Needed for the Postgresql driver
)
var (
x *xorm.Engine
tables []any
initFuncs []func() error
)
// Engine represents a xorm engine or session.
type Engine interface {
Table(tableNameOrBean any) *xorm.Session
Count(...any) (int64, error)
Decr(column string, arg ...any) *xorm.Session
Delete(...any) (int64, error)
Truncate(...any) (int64, error)
Exec(...any) (sql.Result, error)
Find(any, ...any) error
Get(beans ...any) (bool, error)
ID(any) *xorm.Session
In(string, ...any) *xorm.Session
Incr(column string, arg ...any) *xorm.Session
Insert(...any) (int64, error)
Iterate(any, xorm.IterFunc) error
IsTableExist(any) (bool, error)
Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
SQL(any, ...any) *xorm.Session
Where(any, ...any) *xorm.Session
Asc(colNames ...string) *xorm.Session
Desc(colNames ...string) *xorm.Session
Limit(limit int, start ...int) *xorm.Session
NoAutoTime() *xorm.Session
SumInt(bean any, columnName string) (res int64, err error)
Sync(...any) error
Select(string) *xorm.Session
SetExpr(string, any) *xorm.Session
NotIn(string, ...any) *xorm.Session
OrderBy(any, ...any) *xorm.Session
Exist(...any) (bool, error)
Distinct(...string) *xorm.Session
Query(...any) ([]map[string][]byte, error)
Cols(...string) *xorm.Session
Context(ctx context.Context) *xorm.Session
Ping() error
}
// TableInfo returns table's information via an object
func TableInfo(v any) (*schemas.Table, error) {
return x.TableInfo(v)
}
// DumpTables dump tables information
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
return x.DumpTables(tables, w, tp...)
}
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
func RegisterModel(bean any, initFunc ...func() error) {
tables = append(tables, bean)
if len(initFuncs) > 0 && initFunc[0] != nil {
initFuncs = append(initFuncs, initFunc[0])
}
}
func init() {
gonicNames := []string{"SSL", "UID"}
for _, name := range gonicNames {
names.LintGonicMapper[name] = true
}
}
// newXORMEngine returns a new XORM engine from the configuration
func newXORMEngine() (*xorm.Engine, error) {
connStr, err := setting.DBConnStr()
if err != nil {
return nil, err
}
var engine *xorm.Engine
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
// OK whilst we sort out our schema issues - create a schema aware postgres
registerPostgresSchemaDriver()
engine, err = xorm.NewEngine("postgresschema", connStr)
} else {
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
}
if err != nil {
return nil, err
}
if setting.Database.Type.IsMySQL() {
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
}
engine.SetSchema(setting.Database.Schema)
return engine, nil
}
// SyncAllTables sync the schemas of all tables, is required by unit test code
func SyncAllTables() error {
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
WarnIfDatabaseColumnMissed: true,
}, tables...)
return err
}
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine()
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
xormEngine.SetMapper(names.GonicMapper{})
// WARNING: for serv command, MUST remove the output to os.stdout,
// so use log file to instead print to stdout.
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
xormEngine.ShowSQL(setting.Database.LogSQL)
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
xormEngine.SetConnMaxIdleTime(setting.Database.ConnMaxIdleTime)
xormEngine.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 {
xormEngine.AddHook(&SlowQueryHook{
Treshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"),
})
}
errorLogger := log.GetLogger("xorm")
if setting.IsInTesting {
errorLogger = log.GetLogger(log.DEFAULT)
}
xormEngine.AddHook(&ErrorQueryHook{
Logger: errorLogger,
})
xormEngine.AddHook(&TracingHook{})
SetDefaultEngine(ctx, xormEngine)
return nil
}
// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
x = eng
DefaultContext = &Context{
Context: ctx,
e: x,
}
}
// UnsetDefaultEngine closes and unsets the default engine
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
// Global database engine related functions are all racy and there is no graceful close right now.
func UnsetDefaultEngine() {
if x != nil {
_ = x.Close()
x = nil
}
DefaultContext = nil
}
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
// This function must never call .Sync() if the provided migration function fails.
// When called from the "doctor" command, the migration function is a version check
// that prevents the doctor from fixing anything in the database if the migration level
// is different from the expected value.
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
if err = InitEngine(ctx); err != nil {
return err
}
if err = x.Ping(); err != nil {
return err
}
preprocessDatabaseCollation(x)
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
//
// Installation should only be being re-run if users want to recover an old database.
// However, we should think carefully about should we support re-install on an installed instance,
// as there may be other problems due to secret reinitialization.
if err = migrateFunc(x); err != nil {
return fmt.Errorf("migrate: %w", err)
}
if err = SyncAllTables(); err != nil {
return fmt.Errorf("sync database struct error: %w", err)
}
for _, initFunc := range initFuncs {
if err := initFunc(); err != nil {
return fmt.Errorf("initFunc failed: %w", err)
}
}
return nil
}
// NamesToBean return a list of beans or an error
func NamesToBean(names ...string) ([]any, error) {
beans := []any{}
if len(names) == 0 {
beans = append(beans, tables...)
return beans, nil
}
// Need to map provided names to beans...
beanMap := make(map[string]any)
for _, bean := range tables {
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
beanMap[strings.ToLower(x.TableName(bean))] = bean
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
}
gotBean := make(map[any]bool)
for _, name := range names {
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
if !ok {
return nil, fmt.Errorf("no table found that matches: %s", name)
}
if !gotBean[bean] {
beans = append(beans, bean)
gotBean[bean] = true
}
}
return beans, nil
}
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
func DumpDatabase(filePath, dbType string) error {
var tbs []*schemas.Table
for _, t := range tables {
t, err := x.TableInfo(t)
if err != nil {
return err
}
tbs = append(tbs, t)
}
type Version struct {
ID int64 `xorm:"pk autoincr"`
Version int64
}
t, err := x.TableInfo(&Version{})
if err != nil {
return err
}
tbs = append(tbs, t)
if len(dbType) > 0 {
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
}
return x.DumpTablesToFile(tbs, filePath)
}
// MaxBatchInsertSize returns the table's max batch insert size
func MaxBatchInsertSize(bean any) int {
t, err := x.TableInfo(bean)
if err != nil {
return 50
}
return 999 / len(t.ColumnsSeq())
}
// IsTableNotEmpty returns true if table has at least one record
func IsTableNotEmpty(beanOrTableName any) (bool, error) {
return x.Table(beanOrTableName).Exist()
}
// DeleteAllRecords will delete all the records of this table
func DeleteAllRecords(tableName string) error {
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
return err
}
// GetMaxID will return max id of the table
func GetMaxID(beanOrTableName any) (maxID int64, err error) {
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
return maxID, err
}
func SetLogSQL(ctx context.Context, on bool) {
e := GetEngine(ctx)
if x, ok := e.(*xorm.Engine); ok {
x.ShowSQL(on)
} else if sess, ok := e.(*xorm.Session); ok {
sess.Engine().ShowSQL(on)
}
}
type TracingHook struct{}
var _ contexts.Hook = &TracingHook{}
type sqlTask struct{}
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, task := trace.NewTask(c.Ctx, "sql")
ctx = context.WithValue(ctx, sqlTask{}, task)
if isSensitiveSQL(c.Ctx) {
trace.Log(ctx, "query", "[owner-confined query]")
} else {
trace.Log(ctx, "query", c.SQL)
trace.Logf(ctx, "args", "%v", c.Args)
}
return ctx, nil
}
func (TracingHook) AfterProcess(c *contexts.ContextHook) error {
c.Ctx.Value(sqlTask{}).(*trace.Task).End()
return nil
}
type SlowQueryHook struct {
Treshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = &SlowQueryHook{}
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.ExecuteTime >= h.Treshold {
if isSensitiveSQL(c.Ctx) {
h.Logger.Log(8, log.WARN, "[Slow SQL Query] [owner-confined query] - %v", c.ExecuteTime)
} else {
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
}
}
return nil
}
type ErrorQueryHook struct {
Logger log.Logger
}
var _ contexts.Hook = &ErrorQueryHook{}
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
if isSensitiveSQL(c.Ctx) {
h.Logger.Log(8, log.ERROR, "[Error SQL Query] [owner-confined query]")
} else {
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
}
}
return nil
}

View file

@ -0,0 +1,18 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import "context"
type sensitiveSQLKey struct{}
// WithSensitiveSQL suppresses argument, statement and driver-error disclosure
// in the application's query hooks. The caller must separately refuse raw XORM
// SQL logging; custom driver/server auditing remains an owner custody concern.
func WithSensitiveSQL(ctx context.Context) context.Context {
return context.WithValue(ctx, sensitiveSQLKey{}, true)
}
func isSensitiveSQL(ctx context.Context) bool {
sensitive, _ := ctx.Value(sensitiveSQLKey{}).(bool)
return sensitive
}

View file

@ -0,0 +1,103 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"bytes"
"context"
"errors"
"fmt"
"runtime/trace"
"strings"
"testing"
"time"
"forgejo.org/modules/log"
"xorm.io/xorm"
"xorm.io/xorm/contexts"
)
type sensitiveSQLLog struct {
log.Logger
entries []string
}
func (l *sensitiveSQLLog) Log(_ int, _ log.Level, f string, v ...any) {
l.entries = append(l.entries, fmt.Sprintf(f, v...))
}
func TestSensitiveSQLHooks(t *testing.T) {
const secret = "synthetic-query-private-sentinel"
for _, sensitive := range []bool{false, true} {
ctx := context.Background()
if sensitive {
ctx = WithSensitiveSQL(ctx)
}
h := &contexts.ContextHook{Ctx: ctx, SQL: "SELECT '" + secret + "'", Args: []any{secret}, Err: errors.New(secret), ExecuteTime: time.Second}
logger := new(sensitiveSQLLog)
if err := (&SlowQueryHook{Logger: logger}).AfterProcess(h); err != nil {
t.Fatal(err)
}
if err := (&ErrorQueryHook{Logger: logger}).AfterProcess(h); err != nil {
t.Fatal(err)
}
found := strings.Contains(strings.Join(logger.entries, "\n"), secret)
if found == sensitive {
t.Fatal("query hook disclosure/redaction mismatch")
}
var out bytes.Buffer
if err := trace.Start(&out); err != nil {
t.Fatal(err)
}
next, err := (TracingHook{}).BeforeProcess(h)
if err != nil {
trace.Stop()
t.Fatal(err)
}
h.Ctx = next
err = (TracingHook{}).AfterProcess(h)
trace.Stop()
if err != nil {
t.Fatal(err)
}
if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
t.Fatal("trace disclosure/redaction mismatch")
}
}
}
func TestSensitiveSQLActualContextPropagation(t *testing.T) {
engine, ok := GetEngine(DefaultContext).(*xorm.Engine)
if !ok {
t.Fatal("missing actual fixture engine")
}
logger := new(sensitiveSQLLog)
engine.AddHook(&SlowQueryHook{Logger: logger})
engine.AddHook(&ErrorQueryHook{Logger: logger})
engine.AddHook(TracingHook{})
const secret = "synthetic-actual-xorm-private-sentinel"
for _, sensitive := range []bool{false, true} {
logger.entries = nil
ctx := context.Context(DefaultContext)
if sensitive {
ctx = WithSensitiveSQL(ctx)
}
var out bytes.Buffer
if err := trace.Start(&out); err != nil {
t.Fatal(err)
}
_, err := GetEngine(ctx).Exec("SELECT missing_synthetic_function(?)", secret)
trace.Stop()
if err == nil {
t.Fatal("expected actual SQLite function refusal")
}
if len(logger.entries) < 2 {
t.Fatal("actual query hooks did not execute")
}
if strings.Contains(strings.Join(logger.entries, "\n"), secret) == sensitive {
t.Fatal("actual XORM query context did not preserve redaction")
}
if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
t.Fatal("actual XORM trace context did not preserve redaction")
}
}
}

View file

@ -0,0 +1,376 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"reflect"
"runtime/trace"
"strings"
"time"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"xorm.io/xorm"
"xorm.io/xorm/contexts"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas"
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
_ "github.com/lib/pq" // Needed for the Postgresql driver
)
var (
x *xorm.Engine
tables []any
initFuncs []func() error
)
// Engine represents a xorm engine or session.
type Engine interface {
Table(tableNameOrBean any) *xorm.Session
Count(...any) (int64, error)
Decr(column string, arg ...any) *xorm.Session
Delete(...any) (int64, error)
Truncate(...any) (int64, error)
Exec(...any) (sql.Result, error)
Find(any, ...any) error
Get(beans ...any) (bool, error)
ID(any) *xorm.Session
In(string, ...any) *xorm.Session
Incr(column string, arg ...any) *xorm.Session
Insert(...any) (int64, error)
Iterate(any, xorm.IterFunc) error
IsTableExist(any) (bool, error)
Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
SQL(any, ...any) *xorm.Session
Where(any, ...any) *xorm.Session
Asc(colNames ...string) *xorm.Session
Desc(colNames ...string) *xorm.Session
Limit(limit int, start ...int) *xorm.Session
NoAutoTime() *xorm.Session
SumInt(bean any, columnName string) (res int64, err error)
Sync(...any) error
Select(string) *xorm.Session
SetExpr(string, any) *xorm.Session
NotIn(string, ...any) *xorm.Session
OrderBy(any, ...any) *xorm.Session
Exist(...any) (bool, error)
Distinct(...string) *xorm.Session
Query(...any) ([]map[string][]byte, error)
Cols(...string) *xorm.Session
Context(ctx context.Context) *xorm.Session
Ping() error
}
// TableInfo returns table's information via an object
func TableInfo(v any) (*schemas.Table, error) {
return x.TableInfo(v)
}
// DumpTables dump tables information
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
return x.DumpTables(tables, w, tp...)
}
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
func RegisterModel(bean any, initFunc ...func() error) {
tables = append(tables, bean)
if len(initFuncs) > 0 && initFunc[0] != nil {
initFuncs = append(initFuncs, initFunc[0])
}
}
func init() {
gonicNames := []string{"SSL", "UID"}
for _, name := range gonicNames {
names.LintGonicMapper[name] = true
}
}
// newXORMEngine returns a new XORM engine from the configuration
func newXORMEngine() (*xorm.Engine, error) {
connStr, err := setting.DBConnStr()
if err != nil {
return nil, err
}
var engine *xorm.Engine
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
// OK whilst we sort out our schema issues - create a schema aware postgres
registerPostgresSchemaDriver()
engine, err = xorm.NewEngine("postgresschema", connStr)
} else {
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
}
if err != nil {
return nil, err
}
if setting.Database.Type.IsMySQL() {
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
}
engine.SetSchema(setting.Database.Schema)
return engine, nil
}
// SyncAllTables sync the schemas of all tables, is required by unit test code
func SyncAllTables() error {
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
WarnIfDatabaseColumnMissed: true,
}, tables...)
return err
}
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine()
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
xormEngine.SetMapper(names.GonicMapper{})
// WARNING: for serv command, MUST remove the output to os.stdout,
// so use log file to instead print to stdout.
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
xormEngine.ShowSQL(setting.Database.LogSQL)
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
xormEngine.SetConnMaxIdleTime(setting.Database.ConnMaxIdleTime)
xormEngine.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 {
xormEngine.AddHook(&SlowQueryHook{
Treshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"),
})
}
errorLogger := log.GetLogger("xorm")
if setting.IsInTesting {
errorLogger = log.GetLogger(log.DEFAULT)
}
xormEngine.AddHook(&ErrorQueryHook{
Logger: errorLogger,
})
xormEngine.AddHook(&TracingHook{})
SetDefaultEngine(ctx, xormEngine)
return nil
}
// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
x = eng
DefaultContext = &Context{
Context: ctx,
e: x,
}
}
// UnsetDefaultEngine closes and unsets the default engine
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
// Global database engine related functions are all racy and there is no graceful close right now.
func UnsetDefaultEngine() {
if x != nil {
_ = x.Close()
x = nil
}
DefaultContext = nil
}
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
// This function must never call .Sync() if the provided migration function fails.
// When called from the "doctor" command, the migration function is a version check
// that prevents the doctor from fixing anything in the database if the migration level
// is different from the expected value.
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
if err = InitEngine(ctx); err != nil {
return err
}
if err = x.Ping(); err != nil {
return err
}
preprocessDatabaseCollation(x)
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
//
// Installation should only be being re-run if users want to recover an old database.
// However, we should think carefully about should we support re-install on an installed instance,
// as there may be other problems due to secret reinitialization.
if err = migrateFunc(x); err != nil {
return fmt.Errorf("migrate: %w", err)
}
if err = SyncAllTables(); err != nil {
return fmt.Errorf("sync database struct error: %w", err)
}
for _, initFunc := range initFuncs {
if err := initFunc(); err != nil {
return fmt.Errorf("initFunc failed: %w", err)
}
}
return nil
}
// NamesToBean return a list of beans or an error
func NamesToBean(names ...string) ([]any, error) {
beans := []any{}
if len(names) == 0 {
beans = append(beans, tables...)
return beans, nil
}
// Need to map provided names to beans...
beanMap := make(map[string]any)
for _, bean := range tables {
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
beanMap[strings.ToLower(x.TableName(bean))] = bean
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
}
gotBean := make(map[any]bool)
for _, name := range names {
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
if !ok {
return nil, fmt.Errorf("no table found that matches: %s", name)
}
if !gotBean[bean] {
beans = append(beans, bean)
gotBean[bean] = true
}
}
return beans, nil
}
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
func DumpDatabase(filePath, dbType string) error {
var tbs []*schemas.Table
for _, t := range tables {
t, err := x.TableInfo(t)
if err != nil {
return err
}
tbs = append(tbs, t)
}
type Version struct {
ID int64 `xorm:"pk autoincr"`
Version int64
}
t, err := x.TableInfo(&Version{})
if err != nil {
return err
}
tbs = append(tbs, t)
if len(dbType) > 0 {
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
}
return x.DumpTablesToFile(tbs, filePath)
}
// MaxBatchInsertSize returns the table's max batch insert size
func MaxBatchInsertSize(bean any) int {
t, err := x.TableInfo(bean)
if err != nil {
return 50
}
return 999 / len(t.ColumnsSeq())
}
// IsTableNotEmpty returns true if table has at least one record
func IsTableNotEmpty(beanOrTableName any) (bool, error) {
return x.Table(beanOrTableName).Exist()
}
// DeleteAllRecords will delete all the records of this table
func DeleteAllRecords(tableName string) error {
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
return err
}
// GetMaxID will return max id of the table
func GetMaxID(beanOrTableName any) (maxID int64, err error) {
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
return maxID, err
}
func SetLogSQL(ctx context.Context, on bool) {
e := GetEngine(ctx)
if x, ok := e.(*xorm.Engine); ok {
x.ShowSQL(on)
} else if sess, ok := e.(*xorm.Session); ok {
sess.Engine().ShowSQL(on)
}
}
type TracingHook struct{}
var _ contexts.Hook = &TracingHook{}
type sqlTask struct{}
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, task := trace.NewTask(c.Ctx, "sql")
ctx = context.WithValue(ctx, sqlTask{}, task)
trace.Log(ctx, "query", c.SQL)
trace.Logf(ctx, "args", "%v", c.Args)
return ctx, nil
}
func (TracingHook) AfterProcess(c *contexts.ContextHook) error {
c.Ctx.Value(sqlTask{}).(*trace.Task).End()
return nil
}
type SlowQueryHook struct {
Treshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = &SlowQueryHook{}
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.ExecuteTime >= h.Treshold {
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
}
return nil
}
type ErrorQueryHook struct {
Logger log.Logger
}
var _ contexts.Hook = &ErrorQueryHook{}
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
}
return nil
}

View file

@ -0,0 +1,21 @@
import pathlib,subprocess,json,hashlib,time
root=pathlib.Path('/tmp/apps-operation-controls-source-r1');target=root/'models/auth/source_operation.go';original=target.read_text()
runner=pathlib.Path('/tmp/run-apps-operation-tests-r1.py').read_text().replace('/tmp/apps-operation-backend-r1','/tmp/apps-operation-controls-source-r1').replace("'./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQL'","'./models/auth','-run',TEST_FILTER")
controls=[('apply-cas','j.Phase != "prepared" || encodeSourceInventory(rows) != j.BeforeState','j.Phase != "prepared"','TestSourceOperationPrivateDriftRefuses/before-apply'),('inverse-cas','(j.Phase != "applied" && j.Phase != "unchanged") || encodeSourceInventory(rows) != j.PostState','(j.Phase != "applied" && j.Phase != "unchanged")','TestSourceOperationPrivateDriftRefuses/before-inverse'),('linked-delete','if count != 0 {','if false && count != 0 {','TestSourceOperationLinkedSourceRefusesDeletion'),('sqlite-first-write','''if _, err := db.GetEngine(tx).Exec(`UPDATE login_source_operation SET phase = phase WHERE operation_id = ''`); err != nil {
return err
}''','', 'TestSourceOperationDatabaseWriterExclusion')]
results=[]
try:
for label,old,new,filt in controls:
assert old in original,label
target.write_text(original.replace(old,new,1))
script=runner.replace('TEST_FILTER',repr(filt)).replace('/tmp/apps-operation-tests-r1.log',f'/tmp/apps-operation-control-{label}.log').replace('/tmp/apps-operation-tests-r1.json',f'/tmp/apps-operation-control-{label}.json')
p=pathlib.Path(f'/tmp/run-apps-operation-control-{label}.py');p.write_text(script)
outcome=subprocess.run(['python3',str(p)],capture_output=True,text=True)
meta=json.loads(pathlib.Path(f'/tmp/apps-operation-control-{label}.json').read_text())
actual_log=pathlib.Path(f'/tmp/apps-operation-control-{label}.log').read_text()
results.append({'test_assertion_failed':'--- FAIL: TestSourceOperation' in actual_log,'control':label,'expected':'test failure after guard removal','detected':meta['returncode']!=0 and '--- FAIL: TestSourceOperation' in actual_log,'returncode':meta['returncode'],'seconds':meta['seconds']})
print(label,results[-1],flush=True)
finally:
target.write_text(original)
pathlib.Path('/tmp/apps-operation-controls-r1.json').write_text(json.dumps({'source_sha256':hashlib.sha256(original.encode()).hexdigest(),'controls':results},indent=2))

View file

@ -0,0 +1,8 @@
import os,pathlib,subprocess,json,time
r=pathlib.Path('/tmp/apps-operation-backend-r1')
env={'PATH':'/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin','HOME':'/tmp/operation-home','GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOMODCACHE':'/tmp/forgejo-independent-r1/gomodcache','GOCACHE':'/tmp/apps-operation-gocache-r1','GOPATH':'/tmp/operation-gopath','GOVCS':'*:off','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/tmp/operation-test.gitconfig','GIT_TERMINAL_PROMPT':'0','GOMAXPROCS':'4','CGO_ENABLED':'1'}
args=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--ro-bind','/tmp/forgejo-independent-r1/go','/tmp/forgejo-independent-r1/go','--ro-bind','/tmp/forgejo-independent-r1/gomodcache','/tmp/forgejo-independent-r1/gomodcache','--bind',str(r),str(r),'--bind','/tmp/apps-operation-gocache-r1','/tmp/apps-operation-gocache-r1','--chdir',str(r),'--','env','-i']+[k+'='+v for k,v in env.items()]+['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQLHooks','-count=1','-v']
start=time.time()
with open('/tmp/apps-operation-tests-r1.log','w') as out: p=subprocess.run(args,stdout=out,stderr=subprocess.STDOUT)
pathlib.Path('/tmp/apps-operation-tests-r1.json').write_text(json.dumps({'argv':args,'returncode':p.returncode,'seconds':time.time()-start},indent=2))
print(p.returncode);print(pathlib.Path('/tmp/apps-operation-tests-r1.log').read_text()[-9000:])

View file

@ -0,0 +1,8 @@
import os,pathlib,subprocess,json,time
r=pathlib.Path('/tmp/apps-operation-backend-r1')
env={'PATH':'/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin','HOME':'/tmp/operation-home','GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOMODCACHE':'/tmp/forgejo-independent-r1/gomodcache','GOCACHE':'/tmp/apps-operation-gocache-r1','GOPATH':'/tmp/operation-gopath','GOVCS':'*:off','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/tmp/operation-test.gitconfig','GIT_TERMINAL_PROMPT':'0','GOMAXPROCS':'4','CGO_ENABLED':'1'}
args=['bwrap','--unshare-net','--unshare-pid','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--ro-bind','/tmp/forgejo-independent-r1/go','/tmp/forgejo-independent-r1/go','--ro-bind','/tmp/forgejo-independent-r1/gomodcache','/tmp/forgejo-independent-r1/gomodcache','--bind',str(r),str(r),'--bind','/tmp/apps-operation-gocache-r1','/tmp/apps-operation-gocache-r1','--chdir',str(r),'--','env','-i']+[k+'='+v for k,v in env.items()]+['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQL','-count=1','-v']
start=time.time()
with open('/tmp/apps-operation-tests-r1.log','w') as out: p=subprocess.run(args,stdout=out,stderr=subprocess.STDOUT)
pathlib.Path('/tmp/apps-operation-tests-r1.json').write_text(json.dumps({'argv':args,'returncode':p.returncode,'seconds':time.time()-start},indent=2))
print(p.returncode);print(pathlib.Path('/tmp/apps-operation-tests-r1.log').read_text()[-9000:])

View file

@ -0,0 +1,55 @@
{
"source": "actual reviewed Forgejo 11.0.3 extended source at /tmp/forgejo-independent-r1/forgejo",
"candidate_root": "/tmp/apps-operation-backend-r1",
"paths": [
{
"path": "models/auth/source_operation.go",
"before_sha256": null,
"after_sha256": "ef1b07ef6cf454015d00cde8588835f5118d829da91eb0303d37184859345a31",
"preimage_absent": true
},
{
"path": "models/auth/source_operation_test.go",
"before_sha256": null,
"after_sha256": "77d311bca42f48faf3fbdc15c53582e3d8a6d9108a72ed7bb6172e5b88951428",
"preimage_absent": true
},
{
"path": "models/auth/source_operation_internal_test.go",
"before_sha256": null,
"after_sha256": "af0f424d3a1fd2b29793b64baa1fa07284fee7ea0b7721c503db4efac0d871cb",
"preimage_absent": true
},
{
"path": "models/auth/source_operation_process_test.go",
"before_sha256": null,
"after_sha256": "40d33b11a01cb659f0bf6f8b545c4d79910e8c082a789060bcbc9cadeee965bd",
"preimage_absent": true
},
{
"path": "models/db/engine.go",
"before_sha256": "774cedc74e1a8d9ae6f9e5f106c6cbb66f12c11b42e99ef07a7affd03269c3d2",
"after_sha256": "43683804d0c5bc6ead1dba0344c69dd196825cf3d5eb502af8d932a7b8207b96",
"preimage_absent": false
},
{
"path": "models/db/context.go",
"before_sha256": "10774e272206c7a8d6ebfb106b37d535de77f7e96ba57657b4dad960639f8519",
"after_sha256": "d4c4acfcb8987071aecf5cd957b3821c8d2619a2696398d37c2970f2527686f7",
"preimage_absent": false
},
{
"path": "models/db/sensitive_sql.go",
"before_sha256": null,
"after_sha256": "8d4e6b3f08e9fdd919e97e47bd857cbd8935fdcbf367d20b5b8c545f279e796e",
"preimage_absent": true
},
{
"path": "models/db/sensitive_sql_test.go",
"before_sha256": null,
"after_sha256": "de6353c955047c3311747e4777b9f9b2195e9efb19160b971dd758dfc94cf31b",
"preimage_absent": true
}
],
"authority": "source-only low-level private database primitives; no exposed runtime/admission/backend completion"
}

View file

@ -0,0 +1,75 @@
Apps release source r1 requires one source correction before integration. Its
`tools/forgejo-release-build.py:46` build sandbox mounts `/proc` without a PID
namespace. A process running through the exact shipped `sandbox()` can read an
unrelated process's command line, even though that process and its arguments are
outside the build workspace. This breaks the intended separation from ambient
credential-bearing processes. No real credential or host process command line
was inspected: the reproducer ran inside an outer PID/network namespace and
read only a synthetic helper's public canary. Adding only `--unshare-pid` made
the identical read return `DENIED`. The parent-root filesystem traversal control
returned permission denied; no filesystem escape is claimed.
The exact adverse evidence is `proc-boundary-result.json`, with the executable
reproducer and invocation in `proc-boundary-test.py` and
`proc-argv-execution.json`. The synthetic helper exited normally and every owned
execution handle is terminal. The narrow requested correction is PID isolation
in `sandbox()`. This review does not modify the proposal or its owner source.
All other reviewed build/artifact checks passed. The input patch is
`f0bcb2903592874d355e2a403de0c22aeda79b00070f30e7ea02b45106dd67fa`;
the frozen map is
`ec499038fa9a1e612bb81ed63bd67dad2ca52a178bb7bcb89a33bb64832880bb`.
All 71 map entries match current bytes. Applying the patch to the exact canonical
Makefile preimage produces exactly the six frozen postimages. Canonical Apps
remains `944b30ad63ab054cc24617e374e177d34cd572da`, its Makefile and both deployed
values files match the source inventory, and the five proposed added paths are
absent there. Source, Go and Node archives match the lock; source PAX commit and
Node's retained official checksum text agree. The accepted prior independent
review supplies the source/Go upstream-origin checks; no network was used here.
Independent offline checks rehashed both existing source workspaces: each has
628 identical frontend assets, four identical bindata files, the seven exact
previously reviewed source outputs, preserved dependency locks, and the same
110,936,928-byte executable, SHA256
`4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4`.
A fresh execution of the exact candidate's verifier in nested PID/network
isolation extracted all 2,234 public/options/template assets with exact byte
equality, exposed the new CLI flag, and found no ELF interpreter. The executable
sandbox had only the binary and empty output directory; no source/assets/library
fallback could satisfy that test. Seven independent adverse controls refused
outside-/tmp workspaces, existing workspaces, bad archives, repeated extraction,
changed dependency locks, changed reviewed source and changed container binary.
OCI inspection used only a read-only archive, without Docker or a registry.
Every descriptor size, compressed blob digest and uncompressed diff_id matches;
the complete original public base layer descriptor prefix is exact, rootless
runtime configuration and labels agree, and the final layer contains only the
expected directories and exact executable with uid/gid 0 and mode0755.
Archive SHA256 is
`e0e74653321383e133485191f0ac5af53aafb065fb5809e20ede2808cec91539`,
manifest SHA256 is
`9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96`,
and config SHA256 is
`e9e81da43c5ea6895a95b6304337a40874e3275833be5f21fc6ffc104f79a2af`.
The two full builds and two OCI builds remain recorded producer evidence; this
review rechecked their retained outputs and did not repeat expensive builds.
The initial reviewer harness incorrectly required every final-layer timestamp
to equal the image epoch. BuildKit correctly retains older source/base times and
clips later timestamps to the epoch. That harness failure, original script and
result are retained as attempt1. The corrected test checks the source binary's
exact source epoch and each entry's upper bound; attempt2 passes. This is a
reviewer test refinement, with no candidate correction or extra build success.
The later proc controls are a separate test of the same exact source revision.
This packet is an independent technical source review, not owner assent,
architecture acceptance, image admission or deployment authorization. Existing
operational gaps remain: authenticated Access Engine decision enforcement,
private runtime secret input (the proposal is unimplemented), owner integration
and image admission/publication, exact type/case/descriptor re-freeze, current
operation/inverse sessions, durable authenticated journal, authoritative persisted
application before/post/current state, all release/application writer exclusion,
browser/group/login probes, capacity and rollout. Helm omission or rollback
does not delete persisted OAuth sources; image downgrade before verified inverse
is unsafe. Linked users or external logins must not be removed to force inverse.
These remain existing T34/T37 boundaries, not new gates introduced by this review.

View file

@ -0,0 +1,31 @@
{
"README.md": "2f1643212661b1604f9be35300fdc9368b5f8256c94b38b201d35c77d95e27cf",
"controls/archive-tamper.log": "a6eaa5d1f58cf23abd7627f354b782abd98beaab27a8a8bb0d2d876a457823e9",
"controls/binary-change-before-container.log": "e54cbd65d7abda0f1eb0d1c3c942df586dd90046f746081f1ed7ce77f8a9dee8",
"controls/dependency-lock-tamper.log": "4e36b2c28547e306c4e1a69fbc9ebe032ffc282dad8067324f582bee792b20ee",
"controls/existing-workspace.log": "7a09f8a50ea0374f2471bd6e19c6f63c6d9396c6e11adcea25e908c4a9d883bd",
"controls/outside-tmp.log": "15e2ff62c356b04f22bae120faaabb190f40a4b57911971df0e406edbd0ff096",
"controls/repeat-verification.log": "248f3eccad77673747a6bc7d0a746a770c9d00e953bc3e7a9d0d43ed19c9dfb9",
"controls/reviewed-source-tamper.log": "c8a263c96c7e60b8f62e0a995c4a45a61db814860670870b91add69900543467",
"final-input-check.json": "64901fa6e0d6bff50983106605e0a2f690b93c04a5149bb0a9c9f8668b2229d3",
"fresh-process.json": "5bdb80885c005f9886e29a10c579f9f15cf2653d4a1b1259e4ec799e84261645",
"fresh-verification.json": "cd9b24143d73231cc2029b8a8788d32f677fd51255953da6c1aed3d907ed7dca",
"offline-attempt1.json": "b6d6171abf3221dd2da0b79b9631076258a3e638555e3fb2d4f796a53490d8bf",
"offline-attempt1.log": "617277c76cf8c6bbb05100f8ca0f5efd72e77a8c5337a974f70185c292db1b7b",
"offline-attempt2.json": "a04e84668beef61b1ae62093514204bbed3ebb939f13b3461641dedba8b5c009",
"offline-attempt2.log": "0838feceb8317e6e19d3d398fe1476de356ac7aa9ab7348b158406f8b5c28dd4",
"offline-results-attempt1.json": "6eb2430dd38d572abb2a579f2ffd508ad8720e53e7d41dbb7f077178290ca35c",
"offline-results.json": "fb6da7aaaf43fe613d27a025a74ca42e747e7bb5ddd658b62ed7a8a1f3ba3383",
"proc-argv-execution.json": "e32c97e8e56229be96a4e69ddb1d8c47028d876fef2b61e79532e6fc9e26811e",
"proc-argv.log": "d8c7d4ec492dcfbd4bb1584dff74da9c05ec13a730fb58b09138f33d4db00b94",
"proc-boundary-execution.json": "a75dd2e210c88c343b8bfca82f4ca40c7402cf32cbc0f50e5ff8e985db1e9c33",
"proc-boundary-result-attempt1.json": "57878fb236c8c5253cfe5424f7ff136397c0cc08ff700ab0bffcef1e963ac23d",
"proc-boundary-result.json": "d8c7d4ec492dcfbd4bb1584dff74da9c05ec13a730fb58b09138f33d4db00b94",
"proc-boundary-test.py": "45eb2d6eab745ad4e7bfd5a1fc0b33e0757cab20f314b45aaeca15afe76b8703",
"proc-boundary.log": "57878fb236c8c5253cfe5424f7ff136397c0cc08ff700ab0bffcef1e963ac23d",
"public-archives.json": "e67bdfe41e887d380b307e065258838f8c6e2891f63ff842d6a934326f6410f6",
"review-offline-attempt1.py": "1800ca5f24898e3e6fe993461ba50a78ed6bceebe4bc32d87d5527e224330438",
"review-offline.py": "2d4c6f0160eae873fa475042f99f183a818c06925f00de789c5650defb874712",
"review.json": "df2c23df953e1d4c10bd3abd7aea350cc33d8bbfdb2a99d92a436feba0946f73",
"status.json": "3c9427425181504207e5496f52a42aac231b35d413cb1ca70016778fd3da3451"
}

View file

@ -0,0 +1,15 @@
{
"apps_head": "944b30ad63ab054cc24617e374e177d34cd572da",
"canonical_files": {
"Makefile": "a319e264e6f04378f6b3d2c7781860fc4bac1abdb248cffa8c26065e08f2f290",
"helm/forgejo-values.yaml": "cf87a3dccd666bf8870619755afac4af64fc0bee77b0f0ea54718d6384b70d4f",
"helm/forgejo-registry-values.yaml": "a38f551734843c6367448f267c7376b1aeee010927a49690fdd7c1471e0ae450"
},
"Makefile_preimage_equals_canonical_HEAD_and_worktree": true,
"five_added_paths_absent_in_canonical_owner": true,
"all_frozen_entries_still_exact": 71,
"patch_sha256": "f0bcb2903592874d355e2a403de0c22aeda79b00070f30e7ea02b45106dd67fa",
"map_sha256": "ec499038fa9a1e612bb81ed63bd67dad2ca52a178bb7bcb89a33bb64832880bb",
"canonical_owner_changed_by_reviewer": false,
"candidate_changed_by_reviewer": false
}

View file

@ -0,0 +1,65 @@
{
"argv": [
"bwrap",
"--unshare-net",
"--unshare-pid",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--dir",
"/app",
"--ro-bind",
"/tmp/review/verification/source/gitea",
"/app/forgejo",
"--bind",
"/tmp/review/verification/embedded-extracted",
"/results",
"--chdir",
"/tmp",
"--clearenv",
"--setenv",
"PATH",
"/nonexistent",
"--setenv",
"HOME",
"/tmp/home",
"--setenv",
"USER",
"builder",
"--setenv",
"USERNAME",
"builder",
"--setenv",
"GITEA_WORK_DIR",
"/tmp/work",
"--setenv",
"GITEA_CUSTOM",
"/tmp/custom",
"--setenv",
"GITEA_APP_INI",
"/tmp/absent-app.ini",
"--setenv",
"TZ",
"UTC",
"--setenv",
"LC_ALL",
"C",
"--",
"/app/forgejo",
"embedded",
"extract",
"--include-vendored",
"--destination",
"/results",
"**"
],
"driver_pid": null,
"phase": "embedded-extract",
"pid": null,
"returncode": 0,
"seconds": 1.91,
"state": "complete"
}

View file

@ -0,0 +1,13 @@
{
"all_public_options_templates_exact": true,
"architecture_approval": false,
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
"embedded_files": 2234,
"native_admission": false,
"network": "bwrap --unshare-net",
"new_cli_flag_present": true,
"no_dynamic_interpreter": true,
"no_source_assets_libraries_or_host_config_mounted": true,
"source_locks_preserved": true
}

View file

@ -0,0 +1,68 @@
{
"argv": [
"/usr/bin/bwrap",
"--unshare-net",
"--unshare-pid",
"--new-session",
"--die-with-parent",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--bind",
"/tmp/apps-release-independent-review-r1-hkx9xvc0",
"/tmp/review",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1",
"/proposal",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/forgejo-required-claim-values-review-r1",
"/prior-review",
"--ro-bind",
"/tmp/apps-forgejo-release-build-r1/source",
"/source1",
"--ro-bind",
"/tmp/apps-forgejo-release-repro-r1/source",
"/source2",
"--ro-bind",
"/tmp/apps-forgejo-release-repro-r1/image.oci.tar",
"/image.oci.tar",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-release-independent-review-r1/review-offline.py",
"/review-offline.py",
"--chdir",
"/tmp/review",
"--clearenv",
"--setenv",
"PATH",
"/usr/bin:/bin",
"--setenv",
"HOME",
"/tmp/home",
"--",
"/usr/bin/python3",
"-I",
"/review-offline.py"
],
"returncode": 1,
"elapsed_seconds": 6.03,
"owned_pid_terminal": true,
"workspace": "/tmp/apps-release-independent-review-r1-hkx9xvc0",
"network": "unshare-net",
"pid": "unshare-pid",
"host_home_credentials_config_mounted": false
}

View file

@ -0,0 +1,69 @@
{
"argv": [
"/usr/bin/bwrap",
"--unshare-net",
"--unshare-pid",
"--new-session",
"--die-with-parent",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--bind",
"/tmp/apps-release-independent-review-r1-attempt2-86saom6w",
"/tmp/review",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1",
"/proposal",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/forgejo-required-claim-values-review-r1",
"/prior-review",
"--ro-bind",
"/tmp/apps-forgejo-release-build-r1/source",
"/source1",
"--ro-bind",
"/tmp/apps-forgejo-release-repro-r1/source",
"/source2",
"--ro-bind",
"/tmp/apps-forgejo-release-repro-r1/image.oci.tar",
"/image.oci.tar",
"--ro-bind",
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-release-independent-review-r1/review-offline.py",
"/review-offline.py",
"--chdir",
"/tmp/review",
"--clearenv",
"--setenv",
"PATH",
"/usr/bin:/bin",
"--setenv",
"HOME",
"/tmp/home",
"--",
"/usr/bin/python3",
"-I",
"/review-offline.py"
],
"returncode": 0,
"elapsed_seconds": 11.08,
"owned_pid_terminal": true,
"workspace": "/tmp/apps-release-independent-review-r1-attempt2-86saom6w",
"network": "unshare-net",
"pid": "unshare-pid",
"host_home_credentials_config_mounted": false,
"refinement": "Reviewer-only timestamp upper bound correction; attempt1 preserved, no candidate correction or repeated full build."
}

View file

@ -0,0 +1,95 @@
{
"adversarial_refusals": {
"detail": {
"controls": [
{
"case": "outside-tmp",
"refused": true,
"returncode": 1
},
{
"case": "existing-workspace",
"refused": true,
"returncode": 1
},
{
"case": "archive-tamper",
"refused": true,
"returncode": 1
},
{
"case": "repeat-verification",
"refused": true,
"returncode": 1
},
{
"case": "dependency-lock-tamper",
"refused": true,
"returncode": 1
},
{
"case": "reviewed-source-tamper",
"refused": true,
"returncode": 1
},
{
"case": "binary-change-before-container",
"refused": true,
"returncode": 1
}
],
"no_build_container_or_network_action_in_controls": true
},
"passed": true
},
"artifact_reproduction_current_bytes": {
"detail": {
"binary_equal": true,
"bindata_sources_each": 4,
"frontend_files_each": 628,
"public_integrity_bound_npm_packages": 1150
},
"passed": true
},
"fresh_bare_binary_asset_extraction": {
"detail": {
"all_public_options_templates_exact": true,
"architecture_approval": false,
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
"embedded_files": 2234,
"native_admission": false,
"network": "bwrap --unshare-net",
"new_cli_flag_present": true,
"no_dynamic_interpreter": true,
"no_source_assets_libraries_or_host_config_mounted": true,
"source_locks_preserved": true
},
"passed": true
},
"frozen_packet": {
"detail": {
"exact_patch_and_map": true,
"frozen_entries": 71
},
"passed": true
},
"independent_oci_structure_and_content": {
"error": "AssertionError()",
"passed": false
},
"patch_pre_postimages": {
"detail": {
"exact_six_postimages": 6,
"no_other_paths_created": true
},
"passed": true
},
"reviewed_source_binding": {
"detail": {
"both_workspace_outputs_current": true,
"prior_accepted_extension_bound": true
},
"passed": true
}
}