Run server acceptance through CI artifacts and clean task temporaries
studio CI and deployment / verify-and-deploy (pull_request) Successful in 8m10s

This commit is contained in:
2026-10-28 16:25:29 +08:00
parent aa1509df8a
commit 5beddf1d85
3 changed files with 78 additions and 4 deletions
+24
View File
@@ -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
+50
View File
@@ -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"
+4 -4
View File
@@ -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&&current.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)});