diff --git a/.github/skills/experimental/powerpoint/scripts/export_slides.py b/.github/skills/experimental/powerpoint/scripts/export_slides.py index a97c55c06..0a70bb0a8 100644 --- a/.github/skills/experimental/powerpoint/scripts/export_slides.py +++ b/.github/skills/experimental/powerpoint/scripts/export_slides.py @@ -161,7 +161,7 @@ def filter_pdf_pages(pdf_path: Path, pages: list[int], output_path: Path) -> Pat import fitz # noqa: PLC0415 — PyMuPDF except ImportError: logger.error( - "PyMuPDF is required for slide filtering. Install via: pip install pymupdf" + "PyMuPDF is required for slide filtering. Install via: uv pip install pymupdf" ) sys.exit(EXIT_FAILURE) diff --git a/.github/skills/experimental/powerpoint/scripts/export_svg.py b/.github/skills/experimental/powerpoint/scripts/export_svg.py index 1e0533563..672243024 100644 --- a/.github/skills/experimental/powerpoint/scripts/export_svg.py +++ b/.github/skills/experimental/powerpoint/scripts/export_svg.py @@ -173,7 +173,7 @@ def export_pdf_to_svg( import fitz # noqa: F401, PLC0415 — PyMuPDF availability check except ImportError as e: raise PyMuPDFError( - "PyMuPDF is required for SVG export. Install via: pip install pymupdf" + "PyMuPDF is required for SVG export. Install via: uv pip install pymupdf" ) from e try: diff --git a/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py b/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py index d1ebef071..6b3167dfa 100644 --- a/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py +++ b/.github/skills/experimental/powerpoint/scripts/render_pdf_images.py @@ -98,7 +98,7 @@ def render_pages( try: import fitz # noqa: F401, PLC0415 — PyMuPDF availability check except ImportError: - logger.error("PyMuPDF is required. Install via: pip install pymupdf") + logger.error("PyMuPDF is required. Install via: uv pip install pymupdf") sys.exit(EXIT_FAILURE) output_dir.mkdir(parents=True, exist_ok=True) diff --git a/.github/workflows/beval.yml b/.github/workflows/beval.yml index 01e0a2e6a..d712490f9 100644 --- a/.github/workflows/beval.yml +++ b/.github/workflows/beval.yml @@ -38,11 +38,14 @@ jobs: npm ci --prefix evals/beval echo "${{ github.workspace }}/evals/beval/node_modules/.bin" >> "$GITHUB_PATH" + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 + - name: Install beval # beval is hosted under a personal account (vyta) while an org-owned # home is evaluated. The install is pinned to a specific commit SHA to # mitigate supply-chain risk in the interim. - run: pip install --no-cache-dir "beval[all] @ git+https://github.com/vyta/beval.git@d9f46c24f03b0b806d928a8a8ce2fc66d8e470fb#subdirectory=python" + run: uv pip install --system --no-cache-dir "beval[all] @ git+https://github.com/vyta/beval.git@d9f46c24f03b0b806d928a8a8ce2fc66d8e470fb#subdirectory=python" - name: Start agent (TCP) env: diff --git a/.github/workflows/pip-install-lint.yml b/.github/workflows/pip-install-lint.yml new file mode 100644 index 000000000..66e9d2185 --- /dev/null +++ b/.github/workflows/pip-install-lint.yml @@ -0,0 +1,20 @@ +name: Pip Install Lint + +on: + workflow_call: + +jobs: + check-bare-pip-install: + name: Check for bare pip install + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 + with: + python-version: '3.x' + + - name: Run bare pip install lint check + run: python scripts/lint_pip_install.py diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 398209132..9af250c18 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -27,6 +27,10 @@ jobs: with: soft-fail: false + pip-install-lint: + name: Pip Install Lint + uses: ./.github/workflows/pip-install-lint.yml + markdown-lint: name: Markdown Lint uses: ./.github/workflows/markdown-lint.yml diff --git a/scripts/lint_pip_install.py b/scripts/lint_pip_install.py new file mode 100644 index 000000000..6003caebb --- /dev/null +++ b/scripts/lint_pip_install.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026 Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Lint script to detect bare 'pip install' calls. +The repository follows a 'uv-first' Python convention. +""" +import os +import re +import sys + + +EXCLUDE_DIRS = {".git", "evals", ".venv", "venv", "env", "node_modules", "__pycache__"} +EXCLUDE_FILES = {"THIRD-PARTY-NOTICES", "lint_pip_install.py"} +TARGET_DIRS = [".github/workflows", "scripts"] + +bare_pip_pattern = re.compile(r'\bpip3?\s+install\b') +uv_pip_pattern = re.compile(r'\buv\s+pip3?\s+install\b') + +violations = [] +scanned_files = set() + +def should_exclude(filepath): + norm_path = filepath.replace("\\", "/") + + parts = set(norm_path.split("/")) + if parts & EXCLUDE_DIRS: + return True + + for ex_file in EXCLUDE_FILES: + if ex_file in norm_path: + return True + + return False + +def scan_file(filepath): + if filepath in scanned_files: + return + scanned_files.add(filepath) + + if should_exclude(filepath): + return + + try: + with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + for i, line in enumerate(f, 1): + stripped_line = line.strip() + + if not stripped_line or stripped_line.startswith("#"): + continue + + if stripped_line.startswith("name:") or stripped_line.startswith("- name:"): + continue + + if bare_pip_pattern.search(line) and not uv_pip_pattern.search(line): + violations.append(f"{filepath}:{i}: {stripped_line}") + except Exception as e: + print(f"Warning: Could not read {filepath}: {e}") + +def main(): + for target_dir in TARGET_DIRS: + if os.path.isdir(target_dir): + for root, _, files in os.walk(target_dir): + for file in files: + scan_file(os.path.join(root, file)) + + for root, _, files in os.walk("."): + for file in files: + if file.endswith(".py"): + scan_file(os.path.join(root, file)) + + if violations: + print("ERROR: Found bare 'pip install' calls. Use 'uv pip install' instead.") + print("The repo follows a uv-first Python convention.\n") + for v in sorted(set(violations)): + print(f" - {v}") + sys.exit(1) + else: + print("Success: No bare 'pip install' calls found.") + +if __name__ == "__main__": + main()