diff --git a/docs/release-1.9.0-acceptance.md b/docs/release-1.9.0-acceptance.md index e6ec8a9a..64c96d9e 100644 --- a/docs/release-1.9.0-acceptance.md +++ b/docs/release-1.9.0-acceptance.md @@ -14,8 +14,9 @@ whenever human acceptance is needed. - Alpha backend: isolated suite **1,681 passed**, zero failed, four explicit ignores; separate container runtime suite **82 passed**. Optimized binary - SHA256 `a5a6cfc7dcab011963a6f18dc570446b6fa624180d206a108fab27504f4fd41d` - is deployed on dev, yaya and Framework with private rollback backups. + SHA256 `560aa6006cd9ef8be95b1f7831cf3b53854e911622b50022bb4402ce0f8b010a` + is deployed on dev, yaya and Shorty with private rollback backups. Framework + retains the preceding A5 candidate; its earlier acceptance remains recorded. - Frontend: **1,222 passed across 150 files**; production build and real mobile/ desktop media/menu checks pass. Dev serves index SHA256 `c18b24024a78789fe65c74c5ce27efe2125ae869016ab65e33c5a2680b543f17`. @@ -45,9 +46,10 @@ whenever human acceptance is needed. acceptance; retain the completed fresh/nested disposable, public staging issuance/forced renewal and actual yaya state-preservation checks. -- [ ] **Shorty NPM:** shop certificate12/Force SSL are operator accepted and - independently verified; qualify and apply the manual-route migration. Preserve live - management containment and current public app routing. +- [ ] **Shorty NPM:** shop certificate12/Force SSL and manual-route migration pass. Final + corrected backend restart, 302 continuous denial probes and the external + 32-case security matrix pass. Remaining: packaged boot/OTA acceptance. + Preserve management containment and current public app routing. - [ ] **Security:** verify final deployed/booted artifacts against public raw IP, unknown Host/SNI, forged forwarding headers, IPv4/IPv6, assets/RPC/WS; preserve private access, ACME issuance/renewal and public app TLS/WSS. @@ -759,3 +761,26 @@ The release wrapper sets this path and the release harness includes the test. Accepted companion54 APK/metadata replace the stale copies in the packaging staging directory; exact SHA remains ceb58a7d…fab1a1. Final ISO and OTA package acceptance/signatures remain pending. + +### Final demo images and exact-node follow-up + +Shorty final backend restart follow-up passes the external32-case management +denial matrix and trusted public TLS/WSS/official Angor browser fixture. Evidence: +`/tmp/archy-190-final-shorty-public-security-after-restart.log` and +`/tmp/archy-190-final-shorty-angor-browser.log`. This is the known recovered +project, not all35-project discovery. Signed catalog479f6193 is privately active +on dev, yaya and Shorty; no public catalog publication has occurred. + +Built demo images pass isolated real-image qualification: optional upstream DNS +failure returns502 for that service while the main demo remains200; fresh AIUI +provenance, backend healthcheck, four upload protocol/recovery cases, and normal +mobile intro/login/dashboard pass. The test uses a temporary container-only DNS +file; host DNS and the live public demo are untouched. Test: +`tests/lifecycle/demo-images.py`; evidence: +`/tmp/archy-190-demo-images-acceptance-final-2.log`. Earlier failure in the added +DNS test was an incorrect assumption about Podman's generated resolver, repaired +by explicitly mounting the disposable resolver fixture. + +Qualified web image169e59da is built from157c9ec0; backend2722fa29 frome6e46a14. +Public demo deployment remains scheduled after release with rollback. RC2 raw +ISO assembly is running; no boot/install or final OTA pass is claimed yet. diff --git a/tests/lifecycle/demo-images.py b/tests/lifecycle/demo-images.py new file mode 100644 index 00000000..52c5ed9e --- /dev/null +++ b/tests/lifecycle/demo-images.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Opt-in built-demo acceptance; disposable containers, no production data mounts.""" +import pathlib,subprocess,tempfile,uuid,json,time,urllib.request,os +if os.environ.get('ARCHY_ALLOW_DISPOSABLE_CONTAINERS') != '1': + raise SystemExit('Set ARCHY_ALLOW_DISPOSABLE_CONTAINERS=1 for isolated demo tests') +root=pathlib.Path(__file__).resolve().parents[2] +web_image=os.environ['ARCHY_DEMO_WEB_IMAGE'] +backend_image=os.environ['ARCHY_DEMO_BACKEND_IMAGE'] +source_revision=os.environ['ARCHY_DEMO_SOURCE_REVISION'] +assert len(source_revision)==40 and all(c in '0123456789abcdef' for c in source_revision) +uid=uuid.uuid4().hex[:10];net='archy-release-demo-'+uid;backend=net+'-backend';web=net+'-web' +def run(*args):return subprocess.check_output(args,text=True,stderr=subprocess.STDOUT,timeout=60).strip() +dns_fixture=tempfile.TemporaryDirectory(prefix='archy-demo-dns-') +resolv=pathlib.Path(dns_fixture.name)/'resolv.conf' +resolv.write_text('nameserver 127.0.0.1\n') +try: + run('podman','network','create',net) + run('podman','run','-d','--name',backend,'--network',net,'--network-alias','neode-backend','--memory','1g','-p','127.0.0.1::5959','-e','DEMO=1','-e','DEMO_FILE_QUOTA_BYTES=8388608',backend_image) + backend_config=json.loads(run('podman','inspect','--type','container',backend))[0] + backend_ip=next(iter(backend_config['NetworkSettings']['Networks'].values()))['IPAddress'] + run('podman','run','-d','--name',web,'--dns','none','-v',str(resolv)+':/etc/resolv.conf:ro','--add-host','neode-backend:'+backend_ip,'-p','127.0.0.1::2101','--network',net,'--memory','256m','-p','127.0.0.1::80',web_image) + front='http://'+run('podman','port',web,'80/tcp');back='http://'+run('podman','port',backend,'5959/tcp') + end=time.monotonic()+120 + while time.monotonic() {');end=original.index('after(async () => {',start) + original=original[:start]+"before(async () => { origin = "+json.dumps(front)+" })\n"+original[end:] + original=original.replace('const response = await fetch(`${origin}/health`)', 'const response = await fetch('+json.dumps(back+'/health')+')') + # Keep this file beside the source so Node resolves the project's TypeScript. + test=root/'neode-ui/scripts'/('.demo-image-acceptance-'+uid+'.mjs') + test.write_text(original) + try:subprocess.run(['node','--test',str(test)],cwd=root,check=True,timeout=180) + finally:test.unlink(missing_ok=True) + print('PASS all production upload protocol cases through the built nginx image',flush=True) + (pathlib.Path('/tmp/archy-190-demo-image-ports.json')).write_text(json.dumps({'front':front,'back':back,'web':web,'backend':backend})) + # Browser smoke, using the real public-demo login page. + script=r''' +const {createRequire}=require('node:module');const req=createRequire(process.cwd()+'/neode-ui/package.json');const {chromium}=req('@playwright/test'); +(async()=>{const browser=await chromium.launch({headless:true});try {const page=await browser.newPage({viewport:{width:390,height:844}});await page.goto(process.env.DEMO_IMAGE_URL,{waitUntil:'domcontentloaded'});await page.locator('.tap-to-start-content').click({timeout:30000});await page.getByRole('button',{name:'Skip Intro',exact:true}).click();await page.waitForURL('**/onboarding/intro',{timeout:45000});await page.screenshot({path:'/tmp/archy-190-demo-entry.png',fullPage:true});console.log('Demo entry route:',new URL(page.url()).pathname);await page.getByRole('button',{name:'Enter the demo →',exact:true}).click();const password=page.locator('input[type=password]').first();await password.waitFor({timeout:30000});await password.fill('entertoexit');await password.press('Enter');await page.getByText('My Apps',{exact:true}).first().waitFor({timeout:45000});if(await password.isVisible())throw Error('Demo login did not reach dashboard');await page.screenshot({path:'/tmp/archy-190-demo-mobile.png',fullPage:true});console.log('PASS built demo mobile login and dashboard');}finally{await browser.close()}})().catch(e=>{console.error(e);process.exit(1)}); +''' + subprocess.run(['node','-e',script],cwd=root,env=os.environ|{'DEMO_IMAGE_URL':front},check=True,timeout=120) +finally: + for name in [web,backend]:subprocess.run(['podman','rm','-f',name],capture_output=True) + subprocess.run(['podman','network','rm',net],capture_output=True) + dns_fixture.cleanup() + print('Disposable demo containers and network removed',flush=True)