refactor(frontend): 移除 GSAP 并修复首页刷新闪烁 #388
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| merge_group: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| repository-check: | |
| name: Repository rules | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Repository configuration | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import pathlib | |
| import re | |
| pattern = re.compile(r"^\s*-\s+uses:\s+([^\s#]+)", re.MULTILINE) | |
| sha = re.compile(r"^[0-9a-f]{40}$") | |
| invalid = [] | |
| for workflow in sorted(pathlib.Path('.github/workflows').glob('*.y*ml')): | |
| for reference in pattern.findall(workflow.read_text(encoding='utf-8')): | |
| if reference.startswith(('./', 'docker://')): | |
| continue | |
| _, separator, revision = reference.rpartition('@') | |
| if not separator or sha.fullmatch(revision) is None: | |
| invalid.append(f'{workflow}: {reference}') | |
| if invalid: | |
| raise SystemExit('GitHub Actions 必须固定到完整提交 SHA:\n - ' + '\n - '.join(invalid)) | |
| PY | |
| trap 'rm -f .env .env.prod' EXIT | |
| cp .env.example .env | |
| cp .env.prod.example .env.prod | |
| docker compose -f docker-compose-env.yml config --quiet | |
| docker compose -f docker-compose.yml config --quiet | |
| docker compose -f docker-compose-prod.yml config --quiet | |
| backend-quality: | |
| name: Backend quality | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:17.5-alpine | |
| env: | |
| POSTGRES_DB: video | |
| POSTGRES_USER: video | |
| POSTGRES_PASSWORD: video | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -q -U video -d video" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 20 | |
| env: | |
| TEST_DATABASE_URL: postgresql+asyncpg://video:video@127.0.0.1:5432/video | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| with: | |
| version: "0.11.32" | |
| enable-cache: true | |
| cache-dependency-glob: backend/uv.lock | |
| - name: Backend quality gates | |
| working-directory: backend | |
| run: | | |
| uv sync --frozen --dev | |
| uv run --frozen ruff check app tests | |
| uv run --frozen mypy --strict app | |
| uv run --frozen pytest -q | |
| frontend-quality: | |
| name: Frontend quality | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version: "24.15.0" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Pin npm version | |
| run: npm install --global npm@11.19.0 | |
| - name: Frontend quality gates | |
| working-directory: frontend | |
| run: | | |
| npm ci | |
| npm audit --omit=dev --audit-level=high | |
| npm run lint | |
| npm test | |
| npm run build | |
| runtime-smoke: | |
| name: Runtime boundary | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 25 | |
| needs: [repository-check, backend-quality, frontend-quality] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Build and verify runtime topology | |
| run: | | |
| set -Eeuo pipefail | |
| compose=(docker compose --project-name video-server-ci -f docker-compose-env.yml -f docker-compose.yml) | |
| started=0 | |
| cleanup() { | |
| status=$? | |
| trap - EXIT | |
| if ((status != 0 && started == 1)); then | |
| "${compose[@]}" ps --all || true | |
| "${compose[@]}" logs --no-color --tail=200 || true | |
| fi | |
| if ((started == 1)); then | |
| "${compose[@]}" down --volumes --remove-orphans || true | |
| fi | |
| rm -f .env | |
| exit "$status" | |
| } | |
| trap cleanup EXIT | |
| cp .env.example .env | |
| sed -i 's/^ANALYSIS_ENABLED=.*/ANALYSIS_ENABLED=false/' .env | |
| conflicts=() | |
| while IFS= read -r container_name; do | |
| if docker inspect "$container_name" >/dev/null 2>&1; then | |
| conflicts+=("$container_name") | |
| fi | |
| done < <("${compose[@]}" config --format json | python3 -c 'import json,sys; print("\n".join(sorted(service["container_name"] for service in json.load(sys.stdin)["services"].values())))') | |
| if ((${#conflicts[@]} > 0)); then | |
| printf '运行边界烟测拒绝复用已有同名容器:%s\n' "${conflicts[*]}" >&2 | |
| exit 2 | |
| fi | |
| docker build --target runtime --tag video-server:local . | |
| started=1 | |
| "${compose[@]}" up --detach --wait --wait-timeout 240 postgres rabbitmq valkey minio | |
| "${compose[@]}" run --rm --no-deps database-init | |
| "${compose[@]}" run --rm --no-deps rabbitmq-init | |
| "${compose[@]}" run --rm --no-deps minio-init | |
| "${compose[@]}" up --detach --wait --wait-timeout 240 --no-build api frontend outbox worker-download provider-canary worker-report media-runner | |
| python3 - <<'PY' | |
| import json | |
| import urllib.request | |
| def request(origin: str, path: str): | |
| with urllib.request.urlopen(f'{origin}{path}', timeout=10) as response: | |
| return response.status, response.headers.get_content_type(), response.read() | |
| api_origin = 'http://127.0.0.1:8111' | |
| frontend_origin = 'http://127.0.0.1:8101' | |
| status, content_type, body = request(api_origin, '/health/live') | |
| assert status == 200 and json.loads(body) == {'status': 'ok'} | |
| status, content_type, body = request(api_origin, '/health/ready') | |
| assert status == 200 and json.loads(body) == {'status': 'ok', 'service': 'api'} | |
| status, content_type, body = request(api_origin, '/openapi.json') | |
| schema = json.loads(body) | |
| assert status == 200 and content_type == 'application/json' and schema.get('paths') | |
| status, content_type, _ = request(frontend_origin, '/') | |
| assert status == 200 and content_type == 'text/html' | |
| print(f'运行时烟测通过:{len(schema["paths"])} 个 OpenAPI paths') | |
| PY | |
| "${compose[@]}" run --rm --no-deps database-init | |
| required-ci: | |
| name: Required CI | |
| if: always() | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| needs: [repository-check, backend-quality, frontend-quality, runtime-smoke] | |
| steps: | |
| - name: Require every boundary | |
| env: | |
| REPOSITORY_RESULT: ${{ needs.repository-check.result }} | |
| BACKEND_RESULT: ${{ needs.backend-quality.result }} | |
| FRONTEND_RESULT: ${{ needs.frontend-quality.result }} | |
| RUNTIME_RESULT: ${{ needs.runtime-smoke.result }} | |
| run: | | |
| test "$REPOSITORY_RESULT" = success | |
| test "$BACKEND_RESULT" = success | |
| test "$FRONTEND_RESULT" = success | |
| test "$RUNTIME_RESULT" = success |