From 5beddf1d8519f9924dfeccc3b9aff8913690b315 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 28 Oct 2026 16:25:29 +0800 Subject: [PATCH] Run server acceptance through CI artifacts and clean task temporaries --- .gitea/workflows/studio.yml | 24 ++++++++++++++ scripts/verify-studio-production.sh | 50 +++++++++++++++++++++++++++++ scripts/verify-studio-visual.cjs | 8 ++--- 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 scripts/verify-studio-production.sh diff --git a/.gitea/workflows/studio.yml b/.gitea/workflows/studio.yml index fcbecdd..ae2b792 100644 --- a/.gitea/workflows/studio.yml +++ b/.gitea/workflows/studio.yml @@ -5,12 +5,18 @@ on: paths: - 'apps/studio/**' - 'scripts/deploy-studio.sh' + - 'scripts/verify-studio-production.sh' + - 'scripts/verify-studio-library.mjs' + - 'scripts/verify-studio-visual.cjs' - '.gitea/workflows/studio.yml' - 'pnpm-workspace.yaml' pull_request: paths: - 'apps/studio/**' - 'scripts/deploy-studio.sh' + - 'scripts/verify-studio-production.sh' + - 'scripts/verify-studio-library.mjs' + - 'scripts/verify-studio-visual.cjs' - '.gitea/workflows/studio.yml' - 'pnpm-workspace.yaml' workflow_dispatch: @@ -28,3 +34,21 @@ jobs: - name: Release studio if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') run: bash scripts/deploy-studio.sh "${{ gitea.sha }}" + - name: Capture deployed baseline diagnostics (does not validate PR code) + if: gitea.event_name == 'pull_request' + run: bash scripts/verify-studio-production.sh --diagnose + - name: Verify the deployed Studio API and native pages + if: gitea.ref == 'refs/heads/main' && (gitea.event_name == 'push' || gitea.event_name == 'workflow_dispatch') + run: bash scripts/verify-studio-production.sh + - name: Save server acceptance evidence + id: acceptance_evidence + if: always() + uses: actions/upload-artifact@v3 + with: + name: studio-production-acceptance + path: artifacts/studio-acceptance/ + if-no-files-found: ignore + retention-days: 7 + - name: Remove temporary acceptance output after upload + if: always() && steps.acceptance_evidence.outcome == 'success' + run: rm -rf artifacts/studio-acceptance diff --git a/scripts/verify-studio-production.sh b/scripts/verify-studio-production.sh new file mode 100644 index 0000000..1f26adc --- /dev/null +++ b/scripts/verify-studio-production.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Run on ces-control after deployment; never starts a preview web server. +set -Eeuo pipefail +REPO_ROOT=$(git rev-parse --show-toplevel) +RESULT_DIR="$REPO_ROOT/artifacts/studio-acceptance" +MODE=${1:-verify} +[[ "$MODE" == verify || "$MODE" == --diagnose ]] || exit 2 +WIKI_IMAGE=$(docker inspect --format '{{.Config.Image}}' prompt-waterfall-wiki-1) +[[ "$WIKI_IMAGE" =~ ^waterfall-wiki:[0-9a-f]{40}$ ]] || { echo 'Unexpected Wiki image'; exit 1; } +TEST_IMAGE="waterfall-wiki-tests:${WIKI_IMAGE#waterfall-wiki:}" +docker image inspect "$TEST_IMAGE" >/dev/null +mkdir -p "$RESULT_DIR" +chown 1000:1000 "$RESULT_DIR" +run_api(){ + local result_root=$1 script=$2; shift 2 + docker run --rm --volumes-from prompt-waterfall-wiki-1:ro \ + --mount "type=bind,src=$REPO_ROOT,dst=/delivery,readonly" \ + --mount "type=bind,src=$result_root,dst=/tmp/studio-library" \ + -e AUTH_CONFIG_FILE=/auth/config.json -e AUTH_CHECK_BASE_URL=http://8.211.154.134:18131 \ + --entrypoint node "$TEST_IMAGE" dist/auth-cli.cjs run "/delivery/scripts/$script" "$@" +} +# Clean only this task's explicitly named legacy temporary directories. Preserve test data via archive. +if [[ -f /tmp/studio-library/verification.json ]]; then + run_api /tmp/studio-library verify-studio-library.mjs --finalize +fi +if [[ -d /tmp/studio-library-source ]]; then + test -z "$(git -C /tmp/studio-library-source status --porcelain)" || { echo 'Temporary source has unexpected edits; preserved'; exit 1; } + git --git-dir=/tmp/studio-library-delivery.git worktree remove /tmp/studio-library-source +fi +rm -rf /tmp/studio-library-delivery.git /tmp/studio-library-evidence /tmp/studio-library +for old_path in /tmp/studio-library-source /tmp/studio-library-delivery.git /tmp/studio-library-evidence /tmp/studio-library; do + test ! -e "$old_path" || exit 1 +done +printf '{"legacyTemporaryDirectoriesRemoved":true}\n' > "$RESULT_DIR/cleanup.json" +# Always archive only the UUIDs created by this run, including when visual checks fail. +finish(){ + local status=$? + trap - EXIT + if [[ -f "$RESULT_DIR/verification.json" ]]; then + if ! run_api "$RESULT_DIR" verify-studio-library.mjs --finalize; then status=1; fi + fi + exit "$status" +} +trap finish EXIT +run_api "$RESULT_DIR" verify-studio-library.mjs +docker run --rm --volumes-from prompt-waterfall-wiki-1:ro \ + --mount "type=bind,src=$REPO_ROOT,dst=/delivery,readonly" \ + --mount "type=bind,src=$RESULT_DIR,dst=/tmp/studio-library" \ + -e AUTH_CONFIG_FILE=/auth/config.json -e NODE_PATH=/app/node_modules \ + --entrypoint node "$TEST_IMAGE" /delivery/scripts/verify-studio-visual.cjs "$MODE" diff --git a/scripts/verify-studio-visual.cjs b/scripts/verify-studio-visual.cjs index b72de1e..b0562cb 100644 --- a/scripts/verify-studio-visual.cjs +++ b/scripts/verify-studio-visual.cjs @@ -19,7 +19,7 @@ const assert=require('node:assert/strict'); const id='ref-'+String(i).padStart(2,'0');await page.goto(base+'/studio/layouts/'+id,{waitUntil:'networkidle'});await page.locator('article.native-layout').screenshot({path:dir+'/'+id+'.png'}); const m=await page.locator('article.native-layout').evaluate(el=>({id:el.dataset.layoutId,elements:el.querySelectorAll('[data-element]').length,width:el.clientWidth,height:el.clientHeight,overflow:[...el.querySelectorAll('.native-copy')].filter(e=>e.scrollHeight>e.clientHeight+2||e.scrollWidth>e.clientWidth+2).map(e=>e.closest('[data-element]').dataset.element)}));metrics.push(m);await writeFile(dir+'/metrics.json',JSON.stringify({metrics,images,errors},null,2)); } - if(!process.argv.includes('--after-layouts'))assert.deepEqual(metrics.filter(m=>m.overflow.length),[],'Default copy must fit every native slot'); + if(!process.argv.includes('--after-layouts')&&!process.argv.includes('--diagnose'))assert.deepEqual(metrics.filter(m=>m.overflow.length),[],'Default copy must fit every native slot'); await page.goto(base+'/studio',{waitUntil:'networkidle'});assert.equal(await page.locator('.library-item').count(),27);await page.screenshot({path:dir+'/catalog-desktop.png',fullPage:true}); const verify=JSON.parse(await readFile('/tmp/studio-library/verification.json','utf8')); for(const d of verify.results){await page.goto(base+d.url,{waitUntil:'networkidle'});assert.equal(await page.locator('img').count(),1);const current=await page.locator('img').evaluate(img=>({src:img.currentSrc,complete:img.complete,width:img.naturalWidth}));assert.ok(current.complete&¤t.width>0);assert.equal(new URL(current.src).pathname,'/api/preview');await page.locator('article.native-layout').screenshot({path:dir+'/'+d.layoutId+'-saved.png'}); @@ -28,9 +28,9 @@ const assert=require('node:assert/strict'); for(const slug of ['learning-skills-template','layout-selection']){await page.goto(base+'/studio/ideas/'+slug,{waitUntil:'networkidle'});await page.screenshot({path:dir+'/tool-'+slug+'.png',fullPage:true})} await page.setViewportSize({width:390,height:844});await page.goto(base+'/studio',{waitUntil:'networkidle'});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.screenshot({path:dir+'/catalog-mobile.png',fullPage:true}); await page.goto(base+verify.results[0].url,{waitUntil:'networkidle'});await page.screenshot({path:dir+'/standard-mobile.png',fullPage:true}); - await page.goto(base+'/categories?category='+encodeURIComponent('文档入口'),{waitUntil:'domcontentloaded',timeout:60000});await page.getByRole('navigation',{name:'系统标签分类',exact:true}).waitFor({state:'visible'});await page.screenshot({path:dir+'/document-entries.png',fullPage:true}); + await page.goto(base+'/categories?category='+encodeURIComponent('文档入口'),{waitUntil:'domcontentloaded',timeout:60000});await page.getByRole('navigation',{name:'系统标签分类',exact:true}).waitFor({state:'visible'});assert.equal(await page.locator('.discovery-card').count(),3);await page.waitForFunction(()=>Array.from(document.querySelectorAll('.discovery-card img')).every(img=>img.complete&&img.naturalWidth>0),null,{timeout:60000});await page.screenshot({path:dir+'/document-entries.png',fullPage:true}); await writeFile(dir+'/metrics.json',JSON.stringify({metrics,images,errors},null,2)); - assert.equal(images.filter(r=>new URL(r.path,base).pathname.startsWith('/api/media/')).length,0,'Display must never fetch originals');assert.equal(errors.length,0,JSON.stringify(errors));for(const image of images){if(new URL(image.path,base).pathname==='/brand-mark.svg'){assert.equal(image.status,200);assert.match(image.type,/image\/svg\+xml/);continue;}assert.equal(new URL(image.path,base).pathname,'/api/preview');assert.equal(image.status,200);assert.equal(image.type,'image/webp');assert.ok(image.bytes>0&&image.bytes<=220000)} + assert.equal(images.filter(r=>new URL(r.path,base).pathname.startsWith('/api/media/')).length,0,'Display must never fetch originals');if(!process.argv.includes('--diagnose'))assert.equal(errors.length,0,JSON.stringify(errors));for(const image of images){if(new URL(image.path,base).pathname==='/brand-mark.svg'){assert.equal(image.status,200);assert.match(image.type,/image\/svg\+xml/);continue;}assert.equal(new URL(image.path,base).pathname,'/api/preview');assert.equal(image.status,200);assert.equal(image.type,'image/webp');assert.ok(image.bytes>0&&image.bytes<=220000)} await writeFile(dir+'/metrics.json',JSON.stringify({metrics,images,errors},null,2));await browser.close(); - console.log(JSON.stringify({pages:27,errors:errors.length,displayImages:images.length,originalImageRequests:0,overflow:metrics.filter(m=>m.overflow.length)})); + console.log(JSON.stringify({diagnostic:process.argv.includes('--diagnose'),pages:27,errors:errors.length,displayImages:images.length,originalImageRequests:0,overflow:metrics.filter(m=>m.overflow.length)})); })().catch(e=>{console.error(e.message);process.exit(1)});