Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 514e790a78 | |||
| 0849268232 |
@@ -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}));
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user