Fix release CI ratchet and Bugbot integration findings #1303
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 | |
| # Deterministic quality gates that must pass on every push + PR. | |
| # The clean-repo guard is the important one for a public repo: it fails | |
| # the build if mature/explicit prose or locally-generated model guides | |
| # ever leak into tracked source (see scripts/verify_clean_repo.py). | |
| # | |
| # GitHub Actions are pinned to commit SHAs (immutable). Version comments | |
| # are documentation only; do not use moving tags such as @v4 or @latest. | |
| on: | |
| push: | |
| branches: [main, development, dev] | |
| pull_request: | |
| branches: [main, development, dev] | |
| workflow_dispatch: | |
| # Measuring jobs never get write tokens. Artifact upload still works with | |
| # contents: read on this repository. Do not restore pull-request write | |
| # permission on ui-check; that is how untrusted PR code published comments. | |
| permissions: | |
| contents: read | |
| # Cancel superseded PR runs only. Pushes to development/main keep run_id so a | |
| # release or branch push is not cancelled by a later PR. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| guard: | |
| name: Clean-repo guard + Python checks | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.10" | |
| - name: Clean-repo boundary guard | |
| run: python scripts/verify_clean_repo.py | |
| - name: Dependency install/update contract smoke | |
| run: python scripts/check_dependency_contract.py | |
| - name: Documentation links and drift | |
| run: python scripts/check_documentation_links.py | |
| - name: Visible brand and compatibility contract | |
| run: python scripts/check_brand_contract.py | |
| - name: Python syntax check (our modules) | |
| # compileall only compiles (never imports), so it needs no deps and | |
| # skips heavy vendored model code. Covers the surface we edit. | |
| run: python -m compileall -q app/services app/launch.py scripts | |
| python-tests-a: | |
| name: Python tests A | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.10" | |
| cache: pip | |
| cache-dependency-path: | | |
| scripts/ci-python-requirements.txt | |
| scripts/ci-python-torch-cpu.txt | |
| app/requirements.txt | |
| app/runtime/locks/*.txt | |
| - name: Install lightweight test dependencies | |
| run: python -m pip install -r scripts/ci-python-requirements.txt | |
| - name: Install CPU tensor test runtime | |
| run: python -m pip install -r scripts/ci-python-torch-cpu.txt | |
| - name: Cache apt archives | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/hocus-apt-archives | |
| key: ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| ${{ runner.os }}-${{ runner.arch }}-apt-ffmpeg- | |
| - name: Install ffmpeg for media tests | |
| run: | | |
| set -euo pipefail | |
| cache_dir="${HOME}/.cache/hocus-apt-archives" | |
| mkdir -p "${cache_dir}" | |
| sudo mkdir -p /var/cache/apt/archives/partial | |
| if ls "${cache_dir}"/*.deb >/dev/null 2>&1; then | |
| sudo cp "${cache_dir}"/*.deb /var/cache/apt/archives/ || true | |
| fi | |
| sudo apt-get update | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg | |
| cp /var/cache/apt/archives/*.deb "${cache_dir}/" 2>/dev/null || true | |
| - name: Python test suite (shard A) | |
| run: | | |
| set -euo pipefail | |
| python scripts/select_local_tests.py --group python-a > "${RUNNER_TEMP}/shard-a.txt" | |
| test -s "${RUNNER_TEMP}/shard-a.txt" | |
| python -m pytest -q --junitxml=pytest-shard-a.xml $(cat "${RUNNER_TEMP}/shard-a.txt") | |
| - name: Upload shard A report | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: pytest-shard-a | |
| path: pytest-shard-a.xml | |
| if-no-files-found: ignore | |
| python-tests-b: | |
| name: Python tests B | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.10" | |
| cache: pip | |
| cache-dependency-path: | | |
| scripts/ci-python-requirements.txt | |
| scripts/ci-python-torch-cpu.txt | |
| app/requirements.txt | |
| app/runtime/locks/*.txt | |
| - name: Install lightweight test dependencies | |
| run: python -m pip install -r scripts/ci-python-requirements.txt | |
| - name: Install CPU tensor test runtime | |
| run: python -m pip install -r scripts/ci-python-torch-cpu.txt | |
| - name: Cache apt archives | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/hocus-apt-archives | |
| key: ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| ${{ runner.os }}-${{ runner.arch }}-apt-ffmpeg- | |
| - name: Install ffmpeg for media tests | |
| run: | | |
| set -euo pipefail | |
| cache_dir="${HOME}/.cache/hocus-apt-archives" | |
| mkdir -p "${cache_dir}" | |
| sudo mkdir -p /var/cache/apt/archives/partial | |
| if ls "${cache_dir}"/*.deb >/dev/null 2>&1; then | |
| sudo cp "${cache_dir}"/*.deb /var/cache/apt/archives/ || true | |
| fi | |
| sudo apt-get update | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg | |
| cp /var/cache/apt/archives/*.deb "${cache_dir}/" 2>/dev/null || true | |
| - name: Python test suite (shard B) | |
| run: | | |
| set -euo pipefail | |
| python scripts/select_local_tests.py --group python-b > "${RUNNER_TEMP}/shard-b.txt" | |
| test -s "${RUNNER_TEMP}/shard-b.txt" | |
| python -m pytest -q --junitxml=pytest-shard-b.xml $(cat "${RUNNER_TEMP}/shard-b.txt") | |
| - name: Upload shard B report | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: pytest-shard-b | |
| path: pytest-shard-b.xml | |
| if-no-files-found: ignore | |
| ui-check: | |
| name: UI tests + lint + type-check + build | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: "24.18.0" | |
| check-latest: false | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Install UI deps | |
| id: ui-deps | |
| working-directory: ui | |
| run: npm ci | |
| - name: First-party LOC and complexity ratchet | |
| env: | |
| # PRs compare with the PR base. Pushes to development/main compare | |
| # with the previous tip (github.event.before). Never fall back to | |
| # the historical dashboard: that paints merge commits red. | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| BASE_BRANCH: ${{ github.event.pull_request.base.ref }} | |
| SOURCE_BRANCH: ${{ github.event.pull_request.head.ref }} | |
| SOURCE_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }} | |
| SOURCE_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} | |
| EVENT_REPOSITORY: ${{ github.event.repository.full_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then | |
| echo "Cannot resolve code-health base: need pull_request.base.sha or push before" >&2 | |
| exit 2 | |
| fi | |
| set +e | |
| bash scripts/check_code_health_pr_base.sh | tee code-health.md | |
| STATUS=${PIPESTATUS[0]} | |
| set -e | |
| if [ -n "${GITHUB_STEP_SUMMARY:-}" ] && [ -s code-health.md ]; then | |
| cat code-health.md >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit "$STATUS" | |
| - name: Upload code-health report | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: code-health-report | |
| path: | | |
| code-health.md | |
| code-health-integration.json | |
| if-no-files-found: ignore | |
| # Split `npm run check` so a hung runner is visible in the failing step | |
| # instead of one opaque 6-hour job (GitHub's default timeout). | |
| # Collect all validation results even when the ratchet or an earlier | |
| # check fails. A failed step still fails this job and CI required. | |
| - name: UI tests | |
| if: ${{ !cancelled() && steps.ui-deps.outcome == 'success' }} | |
| working-directory: ui | |
| run: npm test | |
| - name: Lint with zero warnings | |
| if: ${{ !cancelled() && steps.ui-deps.outcome == 'success' }} | |
| working-directory: ui | |
| run: npm run lint -- --max-warnings=0 | |
| - name: Type-check, build and bundle budget | |
| if: ${{ !cancelled() && steps.ui-deps.outcome == 'success' }} | |
| working-directory: ui | |
| run: python ../scripts/build_ui.py && python ../scripts/build_ui.py --check && npm run budget | |
| ui-e2e: | |
| name: UI E2E boot (Chromium + simulated API) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: "24.18.0" | |
| check-latest: false | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Install UI deps | |
| working-directory: ui | |
| run: npm ci | |
| - name: Install Playwright Chromium | |
| working-directory: ui | |
| run: npx playwright install --with-deps chromium | |
| - name: Install Chrome for native H.264 and AAC speech export checks | |
| working-directory: ui | |
| run: npx playwright install chrome | |
| - name: Cache apt archives | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/hocus-apt-archives | |
| key: ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-ffmpeg | |
| ${{ runner.os }}-${{ runner.arch }}-apt-ffmpeg- | |
| - name: Install FFmpeg for Linux scene audio finalization | |
| run: | | |
| set -euo pipefail | |
| cache_dir="${HOME}/.cache/hocus-apt-archives" | |
| mkdir -p "${cache_dir}" | |
| sudo mkdir -p /var/cache/apt/archives/partial | |
| if ls "${cache_dir}"/*.deb >/dev/null 2>&1; then | |
| sudo cp "${cache_dir}"/*.deb /var/cache/apt/archives/ || true | |
| fi | |
| sudo apt-get update | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg | |
| cp /var/cache/apt/archives/*.deb "${cache_dir}/" 2>/dev/null || true | |
| - name: UI E2E | |
| working-directory: ui | |
| run: npm run test:e2e | |
| - name: Upload Playwright artifacts | |
| # Passing speech tests also attach screenshots, native JSON and MP4 evidence. | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: ui-e2e-artifacts | |
| path: | | |
| ui/playwright-report | |
| ui/test-results | |
| if-no-files-found: ignore | |
| ui-speech-windows: | |
| name: Speech E2E Windows (real H.264 + AAC) | |
| runs-on: windows-2025 | |
| timeout-minutes: 15 | |
| env: | |
| # This job may not pass via the unsupported-codec assertion. | |
| HOCUSPOCUS_REQUIRE_SPEECH_AAC: "1" | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: "3.10" | |
| cache: pip | |
| cache-dependency-path: | | |
| scripts/ci-python-windows-requirements.txt | |
| app/requirements.txt | |
| app/runtime/locks/*.txt | |
| - name: Runtime isolation and native Windows launcher shell checks | |
| run: | | |
| python -m pip install -r scripts/ci-python-windows-requirements.txt | |
| python -m pytest tests/test_runtime_profiles.py tests/test_launcher_compatibility.py tests/test_ui_distribution.py -q | |
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: "24.18.0" | |
| check-latest: false | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Fresh Windows production UI build and verified repeat start | |
| run: | | |
| python scripts/build_ui.py | |
| python scripts/build_ui.py --check | |
| python scripts/build_ui.py | |
| - name: Install Chromium for screen regression and Edge for native AAC | |
| working-directory: ui | |
| run: npx playwright install chromium msedge | |
| - name: Speech E2E with real export required | |
| working-directory: ui | |
| run: npm run test:e2e -- scene3d-speech.spec.ts scene3d-media-screen.spec.ts --workers=1 | |
| - name: Upload speech evidence | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: ui-speech-windows-artifacts | |
| path: | | |
| ui/playwright-report | |
| ui/test-results | |
| if-no-files-found: ignore | |
| code-health-comment: | |
| name: Code-health PR comment | |
| if: ${{ github.event_name == 'pull_request' && always() && github.event.pull_request.head.repo.full_name == github.repository }} | |
| needs: [ui-check] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| # Publisher runs from the PR *base* tree so untrusted head code cannot | |
| # replace publish_pr_markdown.py while this job has pull-requests:write. | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| ref: ${{ github.event.pull_request.base.sha }} | |
| persist-credentials: false | |
| - name: Download code-health report | |
| continue-on-error: true | |
| uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | |
| with: | |
| name: code-health-report | |
| path: . | |
| - name: Publish score comment | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: python3 scripts/publish_pr_markdown.py --file code-health.md | |
| ci-required: | |
| name: CI required | |
| if: always() | |
| needs: [guard, python-tests-a, python-tests-b, ui-check, ui-e2e, ui-speech-windows] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| - name: Require successful jobs | |
| run: | | |
| python3 scripts/ci_required.py \ | |
| "Clean-repo guard + Python checks=${{ needs.guard.result }}" \ | |
| "Python tests A=${{ needs.python-tests-a.result }}" \ | |
| "Python tests B=${{ needs.python-tests-b.result }}" \ | |
| "UI tests + lint + type-check + build=${{ needs.ui-check.result }}" \ | |
| "UI E2E boot (Chromium + simulated API)=${{ needs.ui-e2e.result }}" \ | |
| "Speech E2E Windows (real H.264 + AAC)=${{ needs.ui-speech-windows.result }}" |