Preserve exported revisions and provide a remote delivery command
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Execute on the confirmed ces-control runner, after user-merged code is deployed.
|
||||
# Uses the existing deployed auth volume and matching CI image; does not build or deploy.
|
||||
set -Eeuo pipefail
|
||||
[[ $# -eq 4 ]] || { echo 'Usage: bash scripts/deliver-studio-series.sh series.json image.png provenance.json output-directory'; exit 2; }
|
||||
ROOT=$(git rev-parse --show-toplevel)
|
||||
INPUT=$(realpath "$1")
|
||||
HERO=$(realpath "$2")
|
||||
PROVENANCE=$(realpath "$3")
|
||||
mkdir -p "$4"
|
||||
OUT=$(realpath "$4")
|
||||
IMAGE=$(docker inspect --format '{{.Config.Image}}' prompt-waterfall-wiki-1)
|
||||
[[ "$IMAGE" =~ ^waterfall-wiki:[0-9a-f]{40}$ ]] || exit 1
|
||||
TEST_IMAGE="waterfall-wiki-tests:${IMAGE#waterfall-wiki:}"
|
||||
docker image inspect "$TEST_IMAGE" >/dev/null
|
||||
chown 1000:1000 "$OUT"
|
||||
COMMON=(--rm --volumes-from prompt-waterfall-wiki-1:ro
|
||||
--mount "type=bind,src=$ROOT,dst=/delivery,readonly"
|
||||
--mount "type=bind,src=$OUT,dst=/output"
|
||||
-e AUTH_CONFIG_FILE=/auth/config.json -e AUTH_CHECK_BASE_URL=http://8.211.154.134:18131)
|
||||
docker run "${COMMON[@]}" \
|
||||
--mount "type=bind,src=$INPUT,dst=/input/series.json,readonly" \
|
||||
--mount "type=bind,src=$HERO,dst=/input/hero.png,readonly" \
|
||||
--mount "type=bind,src=$PROVENANCE,dst=/input/provenance.json,readonly" \
|
||||
--entrypoint node "$TEST_IMAGE" dist/auth-cli.cjs run /delivery/scripts/save-studio-series.mjs /input/series.json /input/hero.png /input/provenance.json /output
|
||||
read -r ID REVISION < <(docker run --rm --mount "type=bind,src=$OUT,dst=/output,readonly" --entrypoint node "$TEST_IMAGE" -e "const d=JSON.parse(require('fs').readFileSync('/output/design.json','utf8'));process.stdout.write(d.id+' '+d.revision+'\n')")
|
||||
[[ "$ID" =~ ^[0-9a-f-]{36}$ && "$REVISION" =~ ^[1-9][0-9]*$ ]] || exit 1
|
||||
docker run "${COMMON[@]}" -e NODE_PATH=/app/node_modules -e "STUDIO_DESIGN_ID=$ID" -e "STUDIO_EXPORT_DIR=/output/revision-$REVISION" \
|
||||
--entrypoint node "$TEST_IMAGE" /delivery/scripts/export-studio-series.cjs
|
||||
@@ -12,6 +12,9 @@ const assert=require('node:assert/strict');
|
||||
const api=async path=>{const r=await fetch(base+path,{headers:{Authorization:'Bearer '+token},redirect:'error'});assert.equal(r.status,200,path);return r.json()};
|
||||
const plan=await api('/api/studio/series/'+id+'/export'),{design}=await api('/api/studio/series/'+id);
|
||||
assert.equal(plan.revision,design.revision);assert.ok(plan.pages.length>=3);assert.equal(new Set(plan.pages.map(p=>p.width+':'+p.height)).size,1,'Use one aspect ratio throughout the series');
|
||||
// A successful revision is immutable on disk. Reuse it only after checking every hash.
|
||||
let previous;try{previous=JSON.parse(await readFile(dir+'/manifest.json','utf8'))}catch(e){if(e.code!=='ENOENT')throw e}
|
||||
if(previous){assert.equal(previous.id,id);assert.equal(previous.revision,plan.revision);assert.equal(previous.files.length,plan.pages.length);for(const [index,file] of previous.files.entries()){assert.equal(file.filename,plan.pages[index].filename);assert.equal(createHash('sha256').update(await readFile(dir+'/'+file.filename)).digest('hex'),file.sha256)}console.log(JSON.stringify({id,revision:plan.revision,existing:true,dir}));return}
|
||||
const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});
|
||||
const evidence={id,revision:plan.revision,url:base+design.url,styleVersion:design.document.style.version,files:[],viewports:[],errors:[]};
|
||||
try{
|
||||
@@ -25,7 +28,7 @@ const assert=require('node:assert/strict');
|
||||
for(const i of m.images){assert.equal(new URL(i.src).pathname,'/api/preview');assert.equal(new URL(i.src).searchParams.get('profile'),'export');assert.ok(i.width>=i.displayWidth*i.zoom&&i.height>=i.displayHeight*i.zoom,'Source image too small for crop/zoom')}
|
||||
// Record actual glyph-rendering fonts, including the CJK fallback used by Chromium.
|
||||
const cdp=await context.newCDPSession(page);await cdp.send('DOM.enable');await cdp.send('CSS.enable');const root=await cdp.send('DOM.getDocument'),nodes=await cdp.send('DOM.querySelectorAll',{nodeId:root.root.nodeId,selector:'.native-copy'}),fonts=[];
|
||||
for(const nodeId of nodes.nodeIds){const text=await cdp.send('DOM.getOuterHTML',{nodeId});if(!text.outerHTML.replace(/<[^>]*>/g,'').trim())continue;const f=await cdp.send('CSS.getPlatformFontsForNode',{nodeId});fonts.push(f.fonts);assert.ok(f.fonts.length,'No rendered fonts')}
|
||||
for(const nodeId of nodes.nodeIds){const text=await cdp.send('DOM.getOuterHTML',{nodeId});if(!text.outerHTML.replace(/<[^>]*>/g,'').trim())continue;const f=await cdp.send('CSS.getPlatformFontsForNode',{nodeId});fonts.push(f.fonts);assert.ok(f.fonts.length,'No rendered fonts');if(/[\u3400-\u9fff]/.test(text.outerHTML))assert.ok(f.fonts.some(font=>/Noto.*CJK.*SC|Source Han|PingFang|Songti|Microsoft YaHei/.test(font.familyName)),'Simplified Chinese font unavailable')}
|
||||
await cdp.detach();const bytes=await page.locator('article.native-layout').screenshot({path:dir+'/'+p.filename,animations:'disabled'}),meta=await sharp(bytes).metadata();assert.equal(meta.width,p.width);assert.equal(meta.height,p.height);evidence.files.push({...p,bytes:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex'),fonts,metrics:m});
|
||||
}
|
||||
assert.equal((await api('/api/studio/series/'+id)).design.revision,plan.revision,'Revision changed during export');assert.deepEqual(evidence.errors,[]);
|
||||
|
||||
Reference in New Issue
Block a user