import pathlib,subprocess,json,urllib.request,time,sqlite3,hashlib,os root=pathlib.Path('/tmp/trilium-release-test-20260913-r1');assert root.resolve()==root env={**os.environ,'COMPOSE_PROJECT_NAME':'trilium-ko-release-test','TRILIUM_KO_IMAGE':'trilium-korean:2.0.0-test','TRILIUM_KO_PORT':'19092','TRILIUM_KO_BIND':'127.0.0.1','TRILIUM_KO_DATA_DIR':'./data-r2'} dockerfile=root/'Dockerfile';original=dockerfile.read_bytes();old=subprocess.check_output(['docker','inspect','--format','{{.Image}}','trilium-ko-release-test-trilium-1'],text=True).strip() try: dockerfile.write_bytes(original+b'\nCMD ["/bin/sh", "-c", "exit 42"]\n') with open(root/'rollback-test.log','w') as log:r=subprocess.run(['bash','install.sh'],cwd=root,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=240) assert r.returncode!=0,'Bad image unexpectedly succeeded' current=subprocess.check_output(['docker','inspect','--format','{{.Image}}','trilium-ko-release-test-trilium-1'],text=True).strip();assert old==current,'Rollback image differs' with urllib.request.urlopen('http://127.0.0.1:19092/api/health-check',timeout=5) as response:assert response.status==200 finally:dockerfile.write_bytes(original) # 가장 최근에 이 검사에서 만든 백업을 별도 경로에 복원합니다. backup=sorted((root/'backups').glob('*/data.tar.gz'))[-1];restore=root/'restore-check';restore.mkdir(exist_ok=False) subprocess.run(['tar','-xzpf',str(backup),'-C',str(restore)],check=True) db=sqlite3.connect('file:'+str(restore/'document.db')+'?mode=ro',uri=True) rows=list(db.execute('SELECT noteId,title,blobId FROM notes WHERE isDeleted=0'));digest=hashlib.sha256(json.dumps(sorted(rows),ensure_ascii=False).encode()).hexdigest();db.close() assert digest==json.loads((root/'before-update.json').read_text())['noteSnapshotSha256'] subprocess.run(['docker','run','-d','--name','trilium-ko-restore-test','-p','127.0.0.1:19093:8080','-v',str(restore)+':/home/node/trilium-data',old],check=True,stdout=subprocess.DEVNULL) for i in range(60): try: with urllib.request.urlopen('http://127.0.0.1:19093/api/setup/status',timeout=2) as response:assert json.load(response)['isInitialized'] break except Exception: if i==59:raise time.sleep(1) report={'forcedFailureRejected':True,'previousImageRestored':True,'backupDataMatched':True,'restoredContainerStarted':True,'backup':str(backup),'restoredNoteSnapshotSha256':digest} (root/'recovery-result.json').write_text(json.dumps(report,indent=2));print(json.dumps(report))