Compare commits

...

2 Commits

Author SHA1 Message Date
codex-bot 514e790a78 Bound review image retention and separate CI navigation budgets
creative workspace review checks / verify (pull_request) Successful in 51m4s
2026-10-31 03:21:23 +08:00
Zayden-Jung 0849268232 Merge pull request 'Fix canvas placement and CI browser verification' (#30) from codex/canvas-node-placement into main
creative workspace CI and deployment / verify-and-deploy (push) Failing after 6m49s
Reviewed-on: #30
2026-10-30 19:06:45 +00:00
5 changed files with 115 additions and 3 deletions
+9 -1
View File
@@ -11,6 +11,8 @@ on:
- 'scripts/integration.sh'
- 'scripts/cleanup-integration.sh'
- 'scripts/test-integration-cleanup.py'
- 'scripts/cleanup-review-images.sh'
- 'scripts/test-review-image-cleanup.py'
concurrency:
group: creative-workspace-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
@@ -25,8 +27,9 @@ jobs:
- name: Check patch formatting
run: |
git diff --check HEAD^ HEAD
bash -n scripts/integration.sh scripts/cleanup-integration.sh
bash -n scripts/integration.sh scripts/cleanup-integration.sh scripts/cleanup-review-images.sh
python3 scripts/test-integration-cleanup.py
python3 scripts/test-review-image-cleanup.py
- name: Verify Studio types, tests, build and smoke
run: docker build -f apps/studio/Dockerfile -t "creative-studio-review:${{ gitea.sha }}" .
- name: Build Wiki integration and runtime images
@@ -45,3 +48,8 @@ jobs:
env:
GITEA_RUN_ID: ${{ gitea.run_id }}
run: bash scripts/cleanup-integration.sh
- name: Remove this run disposable review image tags
if: always()
env:
REVIEW_SHA: ${{ gitea.sha }}
run: bash scripts/cleanup-review-images.sh
@@ -90,6 +90,9 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
);
const page = await context.newPage();
page.setDefaultTimeout(10000);
// Navigation includes document and script loading on the shared runner;
// keep action/readiness checks at 10s and the suite's overall bound unchanged.
page.setDefaultNavigationTimeout(30000);
page.on("pageerror", (error) => errors.push(error.message));
page.on("crash", () => errors.push("Browser renderer crashed"));
const pending = new Set<string>(), network: string[] = [];
@@ -97,12 +100,14 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser)
page.on("requestfinished", request => pending.delete(request.url()));
page.on("requestfailed", request => {pending.delete(request.url());network.push(new URL(request.url()).pathname+": "+request.failure()?.errorText);});
async function navigate(url: string, reload = false) {
const previousSessionReplies = sessionReplies;
const previousSessionReplies = sessionReplies, started = Date.now();
try {
const navigation = await (reload ? page.reload({waitUntil:"domcontentloaded"}) : page.goto(url,{waitUntil:"domcontentloaded"}));
assert.ok(navigation?.ok(), "Navigation returns a successful document");
const documentMs = Date.now() - started;
await page.getByText(signedIn ? "editor@example.com" : "登录你的账户",{exact:true}).first().waitFor({state:"attached"});
assert.ok(sessionReplies > previousSessionReplies, "The new document requests its account fixture and renders the resulting account state");
console.log("Workspace UI navigation timing", JSON.stringify({path:new URL(url).pathname,documentMs,accountReadyMs:Date.now()-started}));
} catch (error) {
const probeStarted=Date.now();
const server=await fetch(url,{signal:AbortSignal.timeout(3000)}).then(async response=>{await response.body?.cancel();return {status:response.status,elapsedMs:Date.now()-probeStarted}}).catch(error=>({error:error instanceof Error?error.name:"unknown",elapsedMs:Date.now()-probeStarted}));
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${REVIEW_SHA:?The exact review commit SHA is required}"
[[ "$REVIEW_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo 'Refusing invalid review SHA' >&2; exit 2; }
# Only this workflow's disposable tags; production tags, volumes and other projects stay out of scope.
for repository in creative-studio-review creative-wiki-tests creative-wiki-review; do
tag="$repository:$REVIEW_SHA"
referenced=$(docker ps -aq | xargs -r docker inspect --format '{{.Image}}')
if ! image=$(docker image inspect --format '{{.Id}}' "$tag" 2>/dev/null); then
echo "No review image: $tag"
continue
fi
if printf '%s\n' "$referenced" | grep -Fxq "$image"; then
echo "Keeping container-referenced review image: $tag"
continue
fi
docker image rm "$tag"
done
+9 -1
View File
@@ -17,7 +17,15 @@ compose(){ docker compose -p "$COMPOSE_PROJECT_NAME" --env-file "$TEST_ENV" "${C
cleanup(){
code=$?
trap - EXIT INT TERM
if [ "$code" -ne 0 ]; then compose logs --tail 160 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}" || true; fi
if [ "$code" -ne 0 ]; then
echo 'Runner pressure at integration failure (before disposable stack cleanup)'
uptime || true
free -m || true
df -h / || true
for resource in cpu memory io; do cat "/proc/pressure/$resource" || true; done
timeout 10s docker stats --no-stream --format '{{.Name}} cpu={{.CPUPerc}} memory={{.MemUsage}} io={{.BlockIO}} pids={{.PIDs}}' || true
compose logs --tail 160 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}" || true
fi
cleanup_code=0
bash scripts/cleanup-integration.sh || cleanup_code=$?
rm -f "$TEST_ENV"
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Check review cleanup scope without Docker or build output."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
SCRIPT = Path(__file__).with_name('cleanup-review-images.sh')
SHA = 'a' * 40
FAKE_DOCKER = '''#!/usr/bin/env python3
import json, os, sys
args=sys.argv[1:]
with open(os.environ['DOCKER_CALLS'], 'a') as output:
output.write(json.dumps(args)+'\\n')
if args[0]=='ps':
if os.environ.get('FAIL_INVENTORY'): sys.exit(1)
if os.environ.get('REFERENCED'): print('container-id')
elif args[0]=='inspect':
print('sha256:review-image')
elif args[:2]==['image','inspect']:
if os.environ.get('MISSING_IMAGE'): sys.exit(1)
print('sha256:review-image')
'''
class ReviewCleanupTests(unittest.TestCase):
def run_cleanup(self, sha=SHA, **extra):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
docker = root / 'docker'
docker.write_text(FAKE_DOCKER)
docker.chmod(0o700)
calls = root / 'calls'
result = subprocess.run(['bash', str(SCRIPT)], capture_output=True, env={
**os.environ, 'PATH': directory + os.pathsep + os.environ['PATH'],
'DOCKER_CALLS': str(calls), 'REVIEW_SHA': sha, **extra})
return result.returncode, [json.loads(line) for line in calls.read_text().splitlines()] if calls.exists() else []
def test_removes_only_exact_review_tags_without_force(self):
code, calls = self.run_cleanup()
self.assertEqual(code, 0)
self.assertEqual([args for args in calls if args[:2] == ['image', 'rm']], [
['image', 'rm', name + ':' + SHA] for name in
['creative-studio-review', 'creative-wiki-tests', 'creative-wiki-review']])
self.assertFalse(any('-f' in args or 'prune' in args or 'volume' in args for args in calls))
def test_invalid_sha_never_contacts_docker(self):
for value in ['', 'main', 'a' * 39, 'a' * 41, 'A' * 40, '*' * 40]:
with self.subTest(value=value):
code, calls = self.run_cleanup(value)
self.assertNotEqual(code, 0)
self.assertEqual(calls, [])
def test_container_referenced_images_are_kept(self):
code, calls = self.run_cleanup(REFERENCED='1')
self.assertEqual(code, 0)
self.assertFalse(any(args[:2] == ['image', 'rm'] for args in calls))
def test_missing_tags_are_a_successful_noop(self):
code, calls = self.run_cleanup(MISSING_IMAGE='1')
self.assertEqual(code, 0)
self.assertFalse(any(args[:2] == ['image', 'rm'] for args in calls))
def test_inventory_failure_prevents_removal(self):
code, calls = self.run_cleanup(FAIL_INVENTORY='1')
self.assertNotEqual(code, 0)
self.assertEqual(len(calls), 1)
if __name__ == '__main__':
unittest.main()