Repository navigation
Merge pull request #8 from InfantLab/1.6-dev: v1.6.0 development (RC1) #174
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/CD Pipeline | |
| on: | |
| push: | |
| branches: [master, main, develop] | |
| pull_request: | |
| branches: [master, main, develop] | |
| release: | |
| types: [published] | |
| schedule: | |
| - cron: "17 4 * * 1" # Mondays: extras-restore-e2e | |
| workflow_dispatch: | |
| jobs: | |
| test: | |
| runs-on: ${{ matrix.os }} | |
| # Normally 4-5 minutes; a stuck apt mirror once held a job for an hour. | |
| timeout-minutes: 45 | |
| # Windows has known SQLite file-locking issues during test teardown | |
| # (tracked separately); keep it informational so it doesn't block CI. | |
| continue-on-error: ${{ matrix.os == 'windows-latest' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ["3.12", "3.13"] | |
| env: | |
| # Otherwise uv follows .python-version (3.13) and the 3.12 jobs test 3.13. | |
| UV_PYTHON: ${{ matrix.python-version }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v10.2.0 # no moving major tags since v8 | |
| with: | |
| enable-cache: true | |
| - name: Install system dependencies (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y ffmpeg libsm6 libxext6 libxrender1 libglib2.0-0 | |
| sudo apt-get install -y cmake build-essential | |
| - name: Install system dependencies (macOS) | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| brew install ffmpeg cmake | |
| - name: Install system dependencies (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: | | |
| choco install ffmpeg cmake | |
| - name: Install dependencies | |
| run: | | |
| uv sync --dev --all-extras | |
| - name: Lint with ruff | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' | |
| run: | | |
| uv run ruff check src/videoannotator tests | |
| - name: Check formatting with ruff | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' | |
| run: | | |
| uv run ruff format --check src/videoannotator tests | |
| - name: Type check with mypy | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' | |
| run: | | |
| uv run mypy src/videoannotator | |
| - name: Test with pytest | |
| env: | |
| # macOS ARM64: numpy 2.x + pyarrow (via datasets/pandas) each bundle their | |
| # own OpenMP runtime, which can double-initialize alongside TensorFlow's | |
| # and abort the process (Fatal Python error: Aborted) on import, not on | |
| # any actual test failure. Same class of issue already documented for | |
| # audio pipelines in docs/installation/INSTALLATION.md's macOS libomp | |
| # note. No-op on Linux/Windows. | |
| KMP_DUPLICATE_LIB_OK: "TRUE" | |
| run: | | |
| uv run pytest -q -m "not real_models and not performance" --cov=src/videoannotator --cov-report=xml --cov-report=term-missing --cov-fail-under=45 | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| files: ./coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| viewer: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: viewer | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.x" | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Lint | |
| run: bun run lint | |
| # Plain `tsc --noEmit` compiles nothing here (tsconfig.json only references). | |
| - name: Typecheck | |
| run: bun run typecheck | |
| - name: Unit tests | |
| run: bun run test:run | |
| # Fails when src/videoannotator/viewer_static/ doesn't match a fresh build of viewer/. | |
| - name: Bundle matches source | |
| working-directory: . | |
| run: bash scripts/build_viewer.sh --check | |
| # Every other job installs the dev group and every extra, which hid a core | |
| # dependency missing from `pip install videoannotator` (httpx, 2026-10-02). | |
| launcher: | |
| # The launcher's sh script against tests/launcher/cases.json (spec 024). No | |
| # engine needed; macOS catches BSD-userland differences (research R12). | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install bats, jq and shellcheck (Ubuntu) | |
| if: runner.os == 'Linux' | |
| run: sudo apt-get update && sudo apt-get install -y bats jq shellcheck | |
| - name: Install bats, jq and shellcheck (macOS) | |
| if: runner.os == 'macOS' | |
| run: brew install bats-core jq shellcheck | |
| - name: shellcheck | |
| run: shellcheck launcher/videoannotator-start launcher/install.sh | |
| - name: bats | |
| run: bats tests/launcher | |
| launcher-windows: | |
| # The PowerShell launcher against the same cases (spec 024). | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install PSScriptAnalyzer and Pester 5 | |
| shell: pwsh | |
| run: | | |
| Install-Module PSScriptAnalyzer -Force -Scope CurrentUser | |
| Install-Module Pester -MinimumVersion 5.5 -MaximumVersion 5.99 -Force -SkipPublisherCheck -Scope CurrentUser | |
| - name: PSScriptAnalyzer | |
| shell: pwsh | |
| run: Invoke-ScriptAnalyzer -Path launcher -Recurse -Severity Warning -EnableExit | |
| - name: Pester (PowerShell 7) | |
| shell: pwsh | |
| run: Invoke-Pester tests/launcher -CI | |
| - name: Pester (Windows PowerShell 5.1) | |
| shell: powershell | |
| run: | | |
| Install-Module Pester -MinimumVersion 5.5 -MaximumVersion 5.99 -Force -SkipPublisherCheck -Scope CurrentUser | |
| Invoke-Pester tests/launcher -CI | |
| e2e-images: | |
| # The slim image, and the CI-only e2e image built from it (Dockerfile.e2e), | |
| # saved for the launcher-e2e jobs to load into Docker and Podman. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Build the slim image | |
| run: docker build -t localhost/videoannotator:ci . | |
| - name: Build the e2e image | |
| run: docker build -f tests/launcher/Dockerfile.e2e -t localhost/videoannotator:e2e . | |
| - name: Save | |
| run: docker save localhost/videoannotator:e2e | gzip > e2e-image.tar.gz | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-image | |
| path: e2e-image.tar.gz | |
| retention-days: 2 | |
| launcher-e2e: | |
| # The launcher end to end with a real engine (spec 024, research R12). | |
| runs-on: ubuntu-latest | |
| needs: [e2e-images] | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| engine: [docker, podman] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: e2e-image | |
| - name: Load the image into ${{ matrix.engine }} | |
| run: gunzip -c e2e-image.tar.gz | ${{ matrix.engine }} load | |
| - name: End to end | |
| run: tests/launcher/test_launcher_e2e.sh ${{ matrix.engine }} | |
| extras-restore-e2e: | |
| # Weekly: an installed pipeline group is restored after the container is | |
| # recreated, from the download cache (spec 024, research R7). Too heavy | |
| # (torch) for every push. | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| needs: [e2e-images] | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: e2e-image | |
| - name: Load the image | |
| run: gunzip -c e2e-image.tar.gz | docker load | |
| - name: Install scene, recreate, restore | |
| run: tests/launcher/test_extras_restore_e2e.sh docker | |
| core-install: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v10.2.0 # no moving major tags since v8 | |
| with: | |
| enable-cache: true | |
| - name: Install core only (no dev group, no extras) | |
| run: | | |
| uv sync --frozen --no-dev | |
| - name: Server starts on a fresh database and serves the API | |
| run: | | |
| export VIDEOANNOTATOR_DB_PATH="$RUNNER_TEMP/va.db" VIDEOANNOTATOR_LOG_DIR="$RUNNER_TEMP/logs" | |
| uv run --no-sync videoannotator server --port 18011 > "$RUNNER_TEMP/server.log" 2>&1 & | |
| for i in $(seq 1 60); do curl -sf localhost:18011/health && break; sleep 2; done | |
| curl -sf localhost:18011/health | |
| curl -sf "localhost:18011/api/v1/pipelines/?include_unavailable=true" > /dev/null | |
| ! grep -E "ERROR|Traceback" "$RUNNER_TEMP/server.log" || { cat "$RUNNER_TEMP/server.log"; exit 1; } | |
| docs: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v10.2.0 # no moving major tags since v8 | |
| with: | |
| enable-cache: true | |
| - name: Relative links resolve | |
| run: python3 scripts/validate_docs_links.py | |
| # The site Pages publishes (docs-site.yml); --strict fails on a broken link or anchor. | |
| - name: Site builds | |
| run: | | |
| uv sync --frozen --only-group docs | |
| uv run --no-sync mkdocs build --strict | |
| integration-test: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| # On releases too: the image and PyPI jobs need it, and skip when it does. | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event_name == 'release' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v10.2.0 # no moving major tags since v8 | |
| with: | |
| enable-cache: true | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y cmake build-essential | |
| sudo apt-get install -y ffmpeg libsm6 libxext6 libxrender1 libglib2.0-0 | |
| - name: Install dependencies | |
| run: | | |
| uv sync --dev --all-extras | |
| - name: Run integration tests | |
| run: | | |
| uv run pytest -q -m "integration and not real_models" | |
| - name: Test CLI interface | |
| run: | | |
| uv run videoannotator --help | |
| uv run videoannotator server --help | |
| performance-test: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v10.2.0 # no moving major tags since v8 | |
| with: | |
| enable-cache: true | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y ffmpeg libsm6 libxext6 libxrender1 libglib2.0-0 | |
| sudo apt-get install -y cmake build-essential | |
| - name: Install dependencies | |
| run: | | |
| uv sync --dev --all-extras | |
| - name: Run performance tests | |
| run: | | |
| uv run pytest -q -m performance --benchmark-only --benchmark-json=benchmark.json | |
| - name: Store benchmark result | |
| uses: benchmark-action/github-action-benchmark@v1 | |
| with: | |
| name: Python Benchmark | |
| tool: "pytest" | |
| output-file-path: benchmark.json | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| auto-push: true | |
| paper: | |
| runs-on: ubuntu-latest | |
| name: Draft JOSS paper | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Build draft PDF | |
| uses: openjournals/openjournals-draft-action@af22ceb4b8f9eff6b3a13c05747ccc2f573cf2e6 # master, 2026-10-01 | |
| with: | |
| journal: joss | |
| paper-path: paper/paper.md | |
| - name: Upload paper PDF | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: paper | |
| path: paper/paper.pdf | |
| security-scan: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: "fs" | |
| scan-ref: "." | |
| format: "sarif" | |
| output: "trivy-results.sarif" | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: "trivy-results.sarif" | |
| build-and-publish: | |
| runs-on: ubuntu-latest | |
| needs: [test, viewer, integration-test] | |
| # Not for release candidates (v1.6.0-rc1): a PyPI version can't be reused. | |
| if: github.event_name == 'release' && github.event.action == 'published' && !contains(github.ref_name, '-') | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Install build dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine | |
| - name: Build package | |
| run: | | |
| python -m build | |
| - name: Check package | |
| run: | | |
| twine check dist/* | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| user: __token__ | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| image: | |
| # The slim image researchers run through the launcher (spec 024, | |
| # contracts/image.md): ghcr.io/infantlab/videoannotator:<version> on | |
| # releases, :latest from the default branch. Docker Hub (docker-build | |
| # below) stays a mirror where its secrets exist. | |
| runs-on: ubuntu-latest | |
| needs: [test, integration-test] | |
| if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'release' && github.event.action == 'published') | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Tags and labels | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ghcr.io/infantlab/videoannotator | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| labels: | | |
| org.opencontainers.image.title=VideoAnnotator | |
| org.opencontainers.image.description=VideoAnnotator, slim: pipelines are installed from the viewer as needed | |
| - name: Build and push (slim) | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| build-args: EXTRAS= | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=ghcr-slim | |
| cache-to: type=gha,mode=max,scope=ghcr-slim | |
| release-launcher: | |
| # The launcher and its installers as release assets, pinned to the | |
| # release's version, which pins the image they run (spec 024, R16). | |
| runs-on: ubuntu-latest | |
| needs: [launcher, launcher-windows] | |
| if: github.event_name == 'release' && github.event.action == 'published' | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Pin the version | |
| run: | | |
| version="${GITHUB_REF_NAME#v}" | |
| sed -i "s/@VERSION@/$version/" launcher/videoannotator-start launcher/videoannotator-start.ps1 \ | |
| launcher/install.sh launcher/install.ps1 | |
| ! grep -l '@VERSION@' launcher/videoannotator-start launcher/videoannotator-start.ps1 \ | |
| launcher/install.sh launcher/install.ps1 | |
| - name: Attach to the release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release upload "$GITHUB_REF_NAME" --clobber \ | |
| launcher/videoannotator-start launcher/videoannotator-start.ps1 \ | |
| launcher/videoannotator-start.cmd launcher/install.sh launcher/install.ps1 | |
| docker-build: | |
| runs-on: ubuntu-latest | |
| needs: [test, integration-test] | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') | |
| env: | |
| DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | |
| DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Check Docker Hub secrets | |
| run: | | |
| if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then | |
| echo "Docker Hub credentials not configured; skipping docker push steps." | |
| else | |
| echo "Docker Hub credentials present." | |
| fi | |
| - name: Login to Docker Hub | |
| if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }} | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: videoannotator/videoannotator | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push Docker image | |
| if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }} | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |