Skip to content

Fix release CI ratchet and Bugbot integration findings #1303

Fix release CI ratchet and Bugbot integration findings

Fix release CI ratchet and Bugbot integration findings #1303

Workflow file for this run

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 }}"